1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui; 16 17 import static android.view.Surface.ROTATION_0; 18 import static android.view.Surface.ROTATION_180; 19 import static android.view.Surface.ROTATION_270; 20 import static android.view.Surface.ROTATION_90; 21 import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; 22 import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; 23 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 24 25 import static com.android.systemui.tuner.TunablePadding.FLAG_END; 26 import static com.android.systemui.tuner.TunablePadding.FLAG_START; 27 28 import android.animation.Animator; 29 import android.animation.AnimatorListenerAdapter; 30 import android.animation.AnimatorSet; 31 import android.animation.ObjectAnimator; 32 import android.animation.ValueAnimator; 33 import android.annotation.Dimension; 34 import android.annotation.NonNull; 35 import android.app.ActivityManager; 36 import android.app.Fragment; 37 import android.content.BroadcastReceiver; 38 import android.content.Context; 39 import android.content.Intent; 40 import android.content.IntentFilter; 41 import android.content.res.ColorStateList; 42 import android.content.res.Configuration; 43 import android.content.res.Resources; 44 import android.graphics.Canvas; 45 import android.graphics.Color; 46 import android.graphics.Matrix; 47 import android.graphics.Paint; 48 import android.graphics.Path; 49 import android.graphics.PixelFormat; 50 import android.graphics.Rect; 51 import android.graphics.RectF; 52 import android.graphics.Region; 53 import android.graphics.drawable.VectorDrawable; 54 import android.hardware.display.DisplayManager; 55 import android.os.Handler; 56 import android.os.HandlerThread; 57 import android.os.SystemProperties; 58 import android.provider.Settings.Secure; 59 import android.util.DisplayMetrics; 60 import android.util.Log; 61 import android.util.MathUtils; 62 import android.view.DisplayCutout; 63 import android.view.DisplayInfo; 64 import android.view.Gravity; 65 import android.view.LayoutInflater; 66 import android.view.Surface; 67 import android.view.View; 68 import android.view.View.OnLayoutChangeListener; 69 import android.view.ViewGroup; 70 import android.view.ViewGroup.LayoutParams; 71 import android.view.ViewTreeObserver; 72 import android.view.WindowManager; 73 import android.view.animation.AccelerateInterpolator; 74 import android.view.animation.Interpolator; 75 import android.view.animation.PathInterpolator; 76 import android.widget.FrameLayout; 77 import android.widget.ImageView; 78 79 import androidx.annotation.VisibleForTesting; 80 81 import com.android.internal.util.Preconditions; 82 import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView; 83 import com.android.systemui.fragments.FragmentHostManager; 84 import com.android.systemui.fragments.FragmentHostManager.FragmentListener; 85 import com.android.systemui.plugins.qs.QS; 86 import com.android.systemui.qs.SecureSetting; 87 import com.android.systemui.shared.system.QuickStepContract; 88 import com.android.systemui.statusbar.phone.CollapsedStatusBarFragment; 89 import com.android.systemui.statusbar.phone.NavigationBarTransitions; 90 import com.android.systemui.statusbar.phone.NavigationModeController; 91 import com.android.systemui.statusbar.phone.StatusBar; 92 import com.android.systemui.tuner.TunablePadding; 93 import com.android.systemui.tuner.TunerService; 94 import com.android.systemui.tuner.TunerService.Tunable; 95 import com.android.systemui.util.leak.RotationUtils; 96 97 import java.util.ArrayList; 98 import java.util.List; 99 100 /** 101 * An overlay that draws screen decorations in software (e.g for rounded corners or display cutout) 102 * for antialiasing and emulation purposes. 103 */ 104 public class ScreenDecorations extends SystemUI implements Tunable, 105 NavigationBarTransitions.DarkIntensityListener { 106 private static final boolean DEBUG = false; 107 private static final String TAG = "ScreenDecorations"; 108 109 public static final String SIZE = "sysui_rounded_size"; 110 public static final String PADDING = "sysui_rounded_content_padding"; 111 private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS = 112 SystemProperties.getBoolean("debug.screenshot_rounded_corners", false); 113 private static final boolean VERBOSE = false; 114 private static final boolean DEBUG_COLOR = DEBUG_SCREENSHOT_ROUNDED_CORNERS; 115 116 private DisplayManager mDisplayManager; 117 private DisplayManager.DisplayListener mDisplayListener; 118 private CameraAvailabilityListener mCameraListener; 119 120 @VisibleForTesting 121 protected int mRoundedDefault; 122 @VisibleForTesting 123 protected int mRoundedDefaultTop; 124 @VisibleForTesting 125 protected int mRoundedDefaultBottom; 126 private View mOverlay; 127 private View mBottomOverlay; 128 private float mDensity; 129 private WindowManager mWindowManager; 130 private int mRotation; 131 private boolean mAssistHintVisible; 132 private DisplayCutoutView mCutoutTop; 133 private DisplayCutoutView mCutoutBottom; 134 private SecureSetting mColorInversionSetting; 135 private boolean mPendingRotationChange; 136 private Handler mHandler; 137 private boolean mAssistHintBlocked = false; 138 private boolean mIsReceivingNavBarColor = false; 139 private boolean mInGesturalMode; 140 private boolean mIsRoundedCornerMultipleRadius; 141 142 private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback = 143 new CameraAvailabilityListener.CameraTransitionCallback() { 144 @Override 145 public void onApplyCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) { 146 // Show the extra protection around the front facing camera if necessary 147 mCutoutTop.setProtection(protectionPath, bounds); 148 mCutoutTop.setShowProtection(true); 149 mCutoutBottom.setProtection(protectionPath, bounds); 150 mCutoutBottom.setShowProtection(true); 151 } 152 153 @Override 154 public void onHideCameraProtection() { 155 // Go back to the regular anti-aliasing 156 mCutoutTop.setShowProtection(false); 157 mCutoutBottom.setShowProtection(false); 158 } 159 }; 160 161 /** 162 * Converts a set of {@link Rect}s into a {@link Region} 163 * 164 * @hide 165 */ rectsToRegion(List<Rect> rects)166 public static Region rectsToRegion(List<Rect> rects) { 167 Region result = Region.obtain(); 168 if (rects != null) { 169 for (Rect r : rects) { 170 if (r != null && !r.isEmpty()) { 171 result.op(r, Region.Op.UNION); 172 } 173 } 174 } 175 return result; 176 } 177 178 @Override start()179 public void start() { 180 mHandler = startHandlerThread(); 181 mHandler.post(this::startOnScreenDecorationsThread); 182 setupStatusBarPaddingIfNeeded(); 183 putComponent(ScreenDecorations.class, this); 184 mInGesturalMode = QuickStepContract.isGesturalMode( 185 Dependency.get(NavigationModeController.class) 186 .addListener(this::handleNavigationModeChange)); 187 } 188 189 @VisibleForTesting handleNavigationModeChange(int navigationMode)190 void handleNavigationModeChange(int navigationMode) { 191 if (!mHandler.getLooper().isCurrentThread()) { 192 mHandler.post(() -> handleNavigationModeChange(navigationMode)); 193 return; 194 } 195 boolean inGesturalMode = QuickStepContract.isGesturalMode(navigationMode); 196 if (mInGesturalMode != inGesturalMode) { 197 mInGesturalMode = inGesturalMode; 198 199 if (mInGesturalMode && mOverlay == null) { 200 setupDecorations(); 201 if (mOverlay != null) { 202 updateLayoutParams(); 203 } 204 } 205 } 206 } 207 208 /** 209 * Returns an animator that animates the given view from start to end over durationMs. Start and 210 * end represent total animation progress: 0 is the start, 1 is the end, 1.1 would be an 211 * overshoot. 212 */ getHandleAnimator(View view, float start, float end, boolean isLeft, long durationMs, Interpolator interpolator)213 Animator getHandleAnimator(View view, float start, float end, boolean isLeft, long durationMs, 214 Interpolator interpolator) { 215 // Note that lerp does allow overshoot, in cases where start and end are outside of [0,1]. 216 float scaleStart = MathUtils.lerp(2f, 1f, start); 217 float scaleEnd = MathUtils.lerp(2f, 1f, end); 218 Animator scaleX = ObjectAnimator.ofFloat(view, View.SCALE_X, scaleStart, scaleEnd); 219 Animator scaleY = ObjectAnimator.ofFloat(view, View.SCALE_Y, scaleStart, scaleEnd); 220 float translationStart = MathUtils.lerp(0.2f, 0f, start); 221 float translationEnd = MathUtils.lerp(0.2f, 0f, end); 222 int xDirection = isLeft ? -1 : 1; 223 Animator translateX = ObjectAnimator.ofFloat(view, View.TRANSLATION_X, 224 xDirection * translationStart * view.getWidth(), 225 xDirection * translationEnd * view.getWidth()); 226 Animator translateY = ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 227 translationStart * view.getHeight(), translationEnd * view.getHeight()); 228 229 AnimatorSet set = new AnimatorSet(); 230 set.play(scaleX).with(scaleY); 231 set.play(scaleX).with(translateX); 232 set.play(scaleX).with(translateY); 233 set.setDuration(durationMs); 234 set.setInterpolator(interpolator); 235 return set; 236 } 237 fade(View view, boolean fadeIn, boolean isLeft)238 private void fade(View view, boolean fadeIn, boolean isLeft) { 239 if (fadeIn) { 240 view.animate().cancel(); 241 view.setAlpha(1f); 242 view.setVisibility(View.VISIBLE); 243 244 // A piecewise spring-like interpolation. 245 // End value in one animator call must match the start value in the next, otherwise 246 // there will be a discontinuity. 247 AnimatorSet anim = new AnimatorSet(); 248 Animator first = getHandleAnimator(view, 0, 1.1f, isLeft, 750, 249 new PathInterpolator(0, 0.45f, .67f, 1f)); 250 Interpolator secondInterpolator = new PathInterpolator(0.33f, 0, 0.67f, 1f); 251 Animator second = getHandleAnimator(view, 1.1f, 0.97f, isLeft, 400, 252 secondInterpolator); 253 Animator third = getHandleAnimator(view, 0.97f, 1.02f, isLeft, 400, 254 secondInterpolator); 255 Animator fourth = getHandleAnimator(view, 1.02f, 1f, isLeft, 400, 256 secondInterpolator); 257 anim.play(first).before(second); 258 anim.play(second).before(third); 259 anim.play(third).before(fourth); 260 anim.start(); 261 } else { 262 view.animate().cancel(); 263 view.animate() 264 .setInterpolator(new AccelerateInterpolator(1.5f)) 265 .setDuration(250) 266 .alpha(0f); 267 } 268 269 } 270 271 /** 272 * Controls the visibility of the assist gesture handles. 273 * 274 * @param visible whether the handles should be shown 275 */ setAssistHintVisible(boolean visible)276 public void setAssistHintVisible(boolean visible) { 277 if (!mHandler.getLooper().isCurrentThread()) { 278 mHandler.post(() -> setAssistHintVisible(visible)); 279 return; 280 } 281 282 if (mAssistHintBlocked && visible) { 283 if (VERBOSE) { 284 Log.v(TAG, "Assist hint blocked, cannot make it visible"); 285 } 286 return; 287 } 288 289 if (mOverlay == null || mBottomOverlay == null) { 290 return; 291 } 292 293 if (mAssistHintVisible != visible) { 294 mAssistHintVisible = visible; 295 296 CornerHandleView assistHintTopLeft = mOverlay.findViewById(R.id.assist_hint_left); 297 CornerHandleView assistHintTopRight = mOverlay.findViewById(R.id.assist_hint_right); 298 CornerHandleView assistHintBottomLeft = mBottomOverlay.findViewById( 299 R.id.assist_hint_left); 300 CornerHandleView assistHintBottomRight = mBottomOverlay.findViewById( 301 R.id.assist_hint_right); 302 303 switch (mRotation) { 304 case RotationUtils.ROTATION_NONE: 305 fade(assistHintBottomLeft, mAssistHintVisible, /* isLeft = */ true); 306 fade(assistHintBottomRight, mAssistHintVisible, /* isLeft = */ false); 307 break; 308 case RotationUtils.ROTATION_LANDSCAPE: 309 fade(assistHintTopRight, mAssistHintVisible, /* isLeft = */ true); 310 fade(assistHintBottomRight, mAssistHintVisible, /* isLeft = */ false); 311 break; 312 case RotationUtils.ROTATION_SEASCAPE: 313 fade(assistHintTopLeft, mAssistHintVisible, /* isLeft = */ false); 314 fade(assistHintBottomLeft, mAssistHintVisible, /* isLeft = */ true); 315 break; 316 case RotationUtils.ROTATION_UPSIDE_DOWN: 317 fade(assistHintTopLeft, mAssistHintVisible, /* isLeft = */ false); 318 fade(assistHintTopRight, mAssistHintVisible, /* isLeft = */ true); 319 break; 320 } 321 } 322 updateWindowVisibilities(); 323 } 324 325 /** 326 * Prevents the assist hint from becoming visible even if `mAssistHintVisible` is true. 327 */ setAssistHintBlocked(boolean blocked)328 public void setAssistHintBlocked(boolean blocked) { 329 if (!mHandler.getLooper().isCurrentThread()) { 330 mHandler.post(() -> setAssistHintBlocked(blocked)); 331 return; 332 } 333 334 mAssistHintBlocked = blocked; 335 if (mAssistHintVisible && mAssistHintBlocked) { 336 hideAssistHandles(); 337 } 338 } 339 340 @VisibleForTesting startHandlerThread()341 Handler startHandlerThread() { 342 HandlerThread thread = new HandlerThread("ScreenDecorations"); 343 thread.start(); 344 return thread.getThreadHandler(); 345 } 346 shouldHostHandles()347 private boolean shouldHostHandles() { 348 return mInGesturalMode; 349 } 350 startOnScreenDecorationsThread()351 private void startOnScreenDecorationsThread() { 352 mRotation = RotationUtils.getExactRotation(mContext); 353 mWindowManager = mContext.getSystemService(WindowManager.class); 354 mIsRoundedCornerMultipleRadius = mContext.getResources().getBoolean( 355 R.bool.config_roundedCornerMultipleRadius); 356 updateRoundedCornerRadii(); 357 if (hasRoundedCorners() || shouldDrawCutout() || shouldHostHandles()) { 358 setupDecorations(); 359 setupCameraListener(); 360 } 361 362 mDisplayListener = new DisplayManager.DisplayListener() { 363 @Override 364 public void onDisplayAdded(int displayId) { 365 // do nothing 366 } 367 368 @Override 369 public void onDisplayRemoved(int displayId) { 370 // do nothing 371 } 372 373 @Override 374 public void onDisplayChanged(int displayId) { 375 final int newRotation = RotationUtils.getExactRotation(mContext); 376 if (mOverlay != null && mBottomOverlay != null && mRotation != newRotation) { 377 // We cannot immediately update the orientation. Otherwise 378 // WindowManager is still deferring layout until it has finished dispatching 379 // the config changes, which may cause divergence between what we draw 380 // (new orientation), and where we are placed on the screen (old orientation). 381 // Instead we wait until either: 382 // - we are trying to redraw. This because WM resized our window and told us to. 383 // - the config change has been dispatched, so WM is no longer deferring layout. 384 mPendingRotationChange = true; 385 if (DEBUG) { 386 Log.i(TAG, "Rotation changed, deferring " + newRotation + ", staying at " 387 + mRotation); 388 } 389 390 mOverlay.getViewTreeObserver().addOnPreDrawListener( 391 new RestartingPreDrawListener(mOverlay, newRotation)); 392 mBottomOverlay.getViewTreeObserver().addOnPreDrawListener( 393 new RestartingPreDrawListener(mBottomOverlay, newRotation)); 394 } 395 updateOrientation(); 396 } 397 }; 398 399 mDisplayManager = (DisplayManager) mContext.getSystemService( 400 Context.DISPLAY_SERVICE); 401 mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); 402 updateOrientation(); 403 } 404 setupDecorations()405 private void setupDecorations() { 406 mOverlay = LayoutInflater.from(mContext) 407 .inflate(R.layout.rounded_corners, null); 408 mCutoutTop = new DisplayCutoutView(mContext, true, 409 this::updateWindowVisibilities, this); 410 ((ViewGroup) mOverlay).addView(mCutoutTop); 411 mBottomOverlay = LayoutInflater.from(mContext) 412 .inflate(R.layout.rounded_corners, null); 413 mCutoutBottom = new DisplayCutoutView(mContext, false, 414 this::updateWindowVisibilities, this); 415 ((ViewGroup) mBottomOverlay).addView(mCutoutBottom); 416 417 mOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 418 mOverlay.setAlpha(0); 419 mOverlay.setForceDarkAllowed(false); 420 421 mBottomOverlay.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); 422 mBottomOverlay.setAlpha(0); 423 mBottomOverlay.setForceDarkAllowed(false); 424 425 updateViews(); 426 427 mWindowManager.addView(mOverlay, getWindowLayoutParams()); 428 mWindowManager.addView(mBottomOverlay, getBottomLayoutParams()); 429 430 DisplayMetrics metrics = new DisplayMetrics(); 431 mWindowManager.getDefaultDisplay().getMetrics(metrics); 432 mDensity = metrics.density; 433 434 Dependency.get(Dependency.MAIN_HANDLER).post( 435 () -> Dependency.get(TunerService.class).addTunable(this, SIZE)); 436 437 // Watch color inversion and invert the overlay as needed. 438 mColorInversionSetting = new SecureSetting(mContext, mHandler, 439 Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED) { 440 @Override 441 protected void handleValueChanged(int value, boolean observedChange) { 442 updateColorInversion(value); 443 } 444 }; 445 mColorInversionSetting.setListening(true); 446 mColorInversionSetting.onChange(false); 447 448 IntentFilter filter = new IntentFilter(); 449 filter.addAction(Intent.ACTION_USER_SWITCHED); 450 mContext.registerReceiver(mIntentReceiver, filter, null /* permission */, mHandler); 451 452 mOverlay.addOnLayoutChangeListener(new OnLayoutChangeListener() { 453 @Override 454 public void onLayoutChange(View v, int left, int top, int right, int bottom, 455 int oldLeft, 456 int oldTop, int oldRight, int oldBottom) { 457 mOverlay.removeOnLayoutChangeListener(this); 458 mOverlay.animate() 459 .alpha(1) 460 .setDuration(1000) 461 .start(); 462 mBottomOverlay.animate() 463 .alpha(1) 464 .setDuration(1000) 465 .start(); 466 } 467 }); 468 469 mOverlay.getViewTreeObserver().addOnPreDrawListener( 470 new ValidatingPreDrawListener(mOverlay)); 471 mBottomOverlay.getViewTreeObserver().addOnPreDrawListener( 472 new ValidatingPreDrawListener(mBottomOverlay)); 473 } 474 setupCameraListener()475 private void setupCameraListener() { 476 Resources res = mContext.getResources(); 477 boolean enabled = res.getBoolean(R.bool.config_enableDisplayCutoutProtection); 478 if (enabled) { 479 mCameraListener = CameraAvailabilityListener.Factory.build(mContext, mHandler::post); 480 mCameraListener.addTransitionCallback(mCameraTransitionCallback); 481 mCameraListener.startListening(); 482 } 483 } 484 485 private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { 486 @Override 487 public void onReceive(Context context, Intent intent) { 488 String action = intent.getAction(); 489 if (action.equals(Intent.ACTION_USER_SWITCHED)) { 490 int newUserId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 491 ActivityManager.getCurrentUser()); 492 // update color inversion setting to the new user 493 mColorInversionSetting.setUserId(newUserId); 494 updateColorInversion(mColorInversionSetting.getValue()); 495 } 496 } 497 }; 498 updateColorInversion(int colorsInvertedValue)499 private void updateColorInversion(int colorsInvertedValue) { 500 int tint = colorsInvertedValue != 0 ? Color.WHITE : Color.BLACK; 501 if (DEBUG_COLOR) { 502 tint = Color.RED; 503 } 504 ColorStateList tintList = ColorStateList.valueOf(tint); 505 ((ImageView) mOverlay.findViewById(R.id.left)).setImageTintList(tintList); 506 ((ImageView) mOverlay.findViewById(R.id.right)).setImageTintList(tintList); 507 ((ImageView) mBottomOverlay.findViewById(R.id.left)).setImageTintList(tintList); 508 ((ImageView) mBottomOverlay.findViewById(R.id.right)).setImageTintList(tintList); 509 mCutoutTop.setColor(tint); 510 mCutoutBottom.setColor(tint); 511 } 512 513 @Override onConfigurationChanged(Configuration newConfig)514 protected void onConfigurationChanged(Configuration newConfig) { 515 mHandler.post(() -> { 516 int oldRotation = mRotation; 517 mPendingRotationChange = false; 518 updateOrientation(); 519 updateRoundedCornerRadii(); 520 if (DEBUG) Log.i(TAG, "onConfigChanged from rot " + oldRotation + " to " + mRotation); 521 if (shouldDrawCutout() && mOverlay == null) { 522 setupDecorations(); 523 } 524 if (mOverlay != null) { 525 // Updating the layout params ensures that ViewRootImpl will call relayoutWindow(), 526 // which ensures that the forced seamless rotation will end, even if we updated 527 // the rotation before window manager was ready (and was still waiting for sending 528 // the updated rotation). 529 updateLayoutParams(); 530 } 531 }); 532 } 533 updateOrientation()534 private void updateOrientation() { 535 Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(), 536 "must call on " + mHandler.getLooper().getThread() 537 + ", but was " + Thread.currentThread()); 538 if (mPendingRotationChange) { 539 return; 540 } 541 int newRotation = RotationUtils.getExactRotation(mContext); 542 if (newRotation != mRotation) { 543 mRotation = newRotation; 544 545 if (mOverlay != null) { 546 updateLayoutParams(); 547 updateViews(); 548 if (mAssistHintVisible) { 549 // If assist handles are visible, hide them without animation and then make them 550 // show once again (with corrected rotation). 551 hideAssistHandles(); 552 setAssistHintVisible(true); 553 } 554 } 555 } 556 } 557 hideAssistHandles()558 private void hideAssistHandles() { 559 if (mOverlay != null && mBottomOverlay != null) { 560 mOverlay.findViewById(R.id.assist_hint_left).setVisibility(View.GONE); 561 mOverlay.findViewById(R.id.assist_hint_right).setVisibility(View.GONE); 562 mBottomOverlay.findViewById(R.id.assist_hint_left).setVisibility(View.GONE); 563 mBottomOverlay.findViewById(R.id.assist_hint_right).setVisibility(View.GONE); 564 mAssistHintVisible = false; 565 } 566 } 567 updateRoundedCornerRadii()568 private void updateRoundedCornerRadii() { 569 final int newRoundedDefault = mContext.getResources().getDimensionPixelSize( 570 com.android.internal.R.dimen.rounded_corner_radius); 571 final int newRoundedDefaultTop = mContext.getResources().getDimensionPixelSize( 572 com.android.internal.R.dimen.rounded_corner_radius_top); 573 final int newRoundedDefaultBottom = mContext.getResources().getDimensionPixelSize( 574 com.android.internal.R.dimen.rounded_corner_radius_bottom); 575 final boolean roundedCornersChanged = mRoundedDefault != newRoundedDefault 576 || mRoundedDefaultBottom != newRoundedDefaultBottom 577 || mRoundedDefaultTop != newRoundedDefaultTop; 578 579 if (roundedCornersChanged) { 580 // If config_roundedCornerMultipleRadius set as true, ScreenDecorations respect the 581 // max(width, height) size of drawable/rounded.xml instead of rounded_corner_radius 582 if (mIsRoundedCornerMultipleRadius) { 583 final VectorDrawable d = (VectorDrawable) mContext.getDrawable(R.drawable.rounded); 584 mRoundedDefault = Math.max(d.getIntrinsicWidth(), d.getIntrinsicHeight()); 585 mRoundedDefaultTop = mRoundedDefaultBottom = mRoundedDefault; 586 } else { 587 mRoundedDefault = newRoundedDefault; 588 mRoundedDefaultTop = newRoundedDefaultTop; 589 mRoundedDefaultBottom = newRoundedDefaultBottom; 590 } 591 } 592 onTuningChanged(SIZE, null); 593 } 594 updateViews()595 private void updateViews() { 596 View topLeft = mOverlay.findViewById(R.id.left); 597 View topRight = mOverlay.findViewById(R.id.right); 598 View bottomLeft = mBottomOverlay.findViewById(R.id.left); 599 View bottomRight = mBottomOverlay.findViewById(R.id.right); 600 601 if (mRotation == RotationUtils.ROTATION_NONE) { 602 updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0); 603 updateView(topRight, Gravity.TOP | Gravity.RIGHT, 90); 604 updateView(bottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270); 605 updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180); 606 } else if (mRotation == RotationUtils.ROTATION_LANDSCAPE) { 607 updateView(topLeft, Gravity.TOP | Gravity.LEFT, 0); 608 updateView(topRight, Gravity.BOTTOM | Gravity.LEFT, 270); 609 updateView(bottomLeft, Gravity.TOP | Gravity.RIGHT, 90); 610 updateView(bottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180); 611 } else if (mRotation == RotationUtils.ROTATION_UPSIDE_DOWN) { 612 updateView(topLeft, Gravity.BOTTOM | Gravity.LEFT, 270); 613 updateView(topRight, Gravity.BOTTOM | Gravity.RIGHT, 180); 614 updateView(bottomLeft, Gravity.TOP | Gravity.LEFT, 0); 615 updateView(bottomRight, Gravity.TOP | Gravity.RIGHT, 90); 616 } else if (mRotation == RotationUtils.ROTATION_SEASCAPE) { 617 updateView(topLeft, Gravity.BOTTOM | Gravity.RIGHT, 180); 618 updateView(topRight, Gravity.TOP | Gravity.RIGHT, 90); 619 updateView(bottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270); 620 updateView(bottomRight, Gravity.TOP | Gravity.LEFT, 0); 621 } 622 623 updateAssistantHandleViews(); 624 mCutoutTop.setRotation(mRotation); 625 mCutoutBottom.setRotation(mRotation); 626 627 updateWindowVisibilities(); 628 } 629 updateAssistantHandleViews()630 private void updateAssistantHandleViews() { 631 View assistHintTopLeft = mOverlay.findViewById(R.id.assist_hint_left); 632 View assistHintTopRight = mOverlay.findViewById(R.id.assist_hint_right); 633 View assistHintBottomLeft = mBottomOverlay.findViewById(R.id.assist_hint_left); 634 View assistHintBottomRight = mBottomOverlay.findViewById(R.id.assist_hint_right); 635 636 final int assistHintVisibility = mAssistHintVisible ? View.VISIBLE : View.INVISIBLE; 637 638 if (mRotation == RotationUtils.ROTATION_NONE) { 639 assistHintTopLeft.setVisibility(View.GONE); 640 assistHintTopRight.setVisibility(View.GONE); 641 assistHintBottomLeft.setVisibility(assistHintVisibility); 642 assistHintBottomRight.setVisibility(assistHintVisibility); 643 updateView(assistHintBottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270); 644 updateView(assistHintBottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180); 645 } else if (mRotation == RotationUtils.ROTATION_LANDSCAPE) { 646 assistHintTopLeft.setVisibility(View.GONE); 647 assistHintTopRight.setVisibility(assistHintVisibility); 648 assistHintBottomLeft.setVisibility(View.GONE); 649 assistHintBottomRight.setVisibility(assistHintVisibility); 650 updateView(assistHintTopRight, Gravity.BOTTOM | Gravity.LEFT, 270); 651 updateView(assistHintBottomRight, Gravity.BOTTOM | Gravity.RIGHT, 180); 652 } else if (mRotation == RotationUtils.ROTATION_UPSIDE_DOWN) { 653 assistHintTopLeft.setVisibility(assistHintVisibility); 654 assistHintTopRight.setVisibility(assistHintVisibility); 655 assistHintBottomLeft.setVisibility(View.GONE); 656 assistHintBottomRight.setVisibility(View.GONE); 657 updateView(assistHintTopLeft, Gravity.BOTTOM | Gravity.LEFT, 270); 658 updateView(assistHintTopRight, Gravity.BOTTOM | Gravity.RIGHT, 180); 659 } else if (mRotation == RotationUtils.ROTATION_SEASCAPE) { 660 assistHintTopLeft.setVisibility(assistHintVisibility); 661 assistHintTopRight.setVisibility(View.GONE); 662 assistHintBottomLeft.setVisibility(assistHintVisibility); 663 assistHintBottomRight.setVisibility(View.GONE); 664 updateView(assistHintTopLeft, Gravity.BOTTOM | Gravity.RIGHT, 180); 665 updateView(assistHintBottomLeft, Gravity.BOTTOM | Gravity.LEFT, 270); 666 } 667 } 668 updateView(View v, int gravity, int rotation)669 private void updateView(View v, int gravity, int rotation) { 670 ((FrameLayout.LayoutParams) v.getLayoutParams()).gravity = gravity; 671 v.setRotation(rotation); 672 } 673 updateWindowVisibilities()674 private void updateWindowVisibilities() { 675 updateWindowVisibility(mOverlay); 676 updateWindowVisibility(mBottomOverlay); 677 } 678 updateWindowVisibility(View overlay)679 private void updateWindowVisibility(View overlay) { 680 boolean visibleForCutout = shouldDrawCutout() 681 && overlay.findViewById(R.id.display_cutout).getVisibility() == View.VISIBLE; 682 boolean visibleForRoundedCorners = hasRoundedCorners(); 683 boolean visibleForHandles = overlay.findViewById(R.id.assist_hint_left).getVisibility() 684 == View.VISIBLE || overlay.findViewById(R.id.assist_hint_right).getVisibility() 685 == View.VISIBLE; 686 overlay.setVisibility(visibleForCutout || visibleForRoundedCorners || visibleForHandles 687 ? View.VISIBLE : View.GONE); 688 } 689 hasRoundedCorners()690 private boolean hasRoundedCorners() { 691 return mRoundedDefault > 0 || mRoundedDefaultBottom > 0 || mRoundedDefaultTop > 0 692 || mIsRoundedCornerMultipleRadius; 693 } 694 shouldDrawCutout()695 private boolean shouldDrawCutout() { 696 return shouldDrawCutout(mContext); 697 } 698 shouldDrawCutout(Context context)699 static boolean shouldDrawCutout(Context context) { 700 return context.getResources().getBoolean( 701 com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout); 702 } 703 704 setupStatusBarPaddingIfNeeded()705 private void setupStatusBarPaddingIfNeeded() { 706 // TODO: This should be moved to a more appropriate place, as it is not related to the 707 // screen decorations overlay. 708 int padding = mContext.getResources().getDimensionPixelSize( 709 R.dimen.rounded_corner_content_padding); 710 if (padding != 0) { 711 setupStatusBarPadding(padding); 712 } 713 714 } 715 setupStatusBarPadding(int padding)716 private void setupStatusBarPadding(int padding) { 717 // Add some padding to all the content near the edge of the screen. 718 StatusBar sb = getComponent(StatusBar.class); 719 View statusBar = (sb != null ? sb.getStatusBarWindow() : null); 720 if (statusBar != null) { 721 TunablePadding.addTunablePadding(statusBar.findViewById(R.id.keyguard_header), PADDING, 722 padding, FLAG_END); 723 724 FragmentHostManager fragmentHostManager = FragmentHostManager.get(statusBar); 725 fragmentHostManager.addTagListener(CollapsedStatusBarFragment.TAG, 726 new TunablePaddingTagListener(padding, R.id.status_bar)); 727 fragmentHostManager.addTagListener(QS.TAG, 728 new TunablePaddingTagListener(padding, R.id.header)); 729 } 730 } 731 732 @VisibleForTesting getWindowLayoutParams()733 WindowManager.LayoutParams getWindowLayoutParams() { 734 final WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 735 ViewGroup.LayoutParams.MATCH_PARENT, 736 LayoutParams.WRAP_CONTENT, 737 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, 738 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE 739 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL 740 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH 741 | WindowManager.LayoutParams.FLAG_SLIPPERY 742 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, 743 PixelFormat.TRANSLUCENT); 744 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS 745 | WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; 746 747 if (!DEBUG_SCREENSHOT_ROUNDED_CORNERS) { 748 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY; 749 } 750 751 lp.setTitle("ScreenDecorOverlay"); 752 if (mRotation == RotationUtils.ROTATION_SEASCAPE 753 || mRotation == RotationUtils.ROTATION_UPSIDE_DOWN) { 754 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT; 755 } else { 756 lp.gravity = Gravity.TOP | Gravity.LEFT; 757 } 758 lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; 759 if (isLandscape(mRotation)) { 760 lp.width = WRAP_CONTENT; 761 lp.height = MATCH_PARENT; 762 } 763 lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC; 764 return lp; 765 } 766 getBottomLayoutParams()767 private WindowManager.LayoutParams getBottomLayoutParams() { 768 WindowManager.LayoutParams lp = getWindowLayoutParams(); 769 lp.setTitle("ScreenDecorOverlayBottom"); 770 if (mRotation == RotationUtils.ROTATION_SEASCAPE 771 || mRotation == RotationUtils.ROTATION_UPSIDE_DOWN) { 772 lp.gravity = Gravity.TOP | Gravity.LEFT; 773 } else { 774 lp.gravity = Gravity.BOTTOM | Gravity.RIGHT; 775 } 776 return lp; 777 } 778 updateLayoutParams()779 private void updateLayoutParams() { 780 mWindowManager.updateViewLayout(mOverlay, getWindowLayoutParams()); 781 mWindowManager.updateViewLayout(mBottomOverlay, getBottomLayoutParams()); 782 } 783 784 @Override onTuningChanged(String key, String newValue)785 public void onTuningChanged(String key, String newValue) { 786 mHandler.post(() -> { 787 if (mOverlay == null) return; 788 if (SIZE.equals(key)) { 789 int size = mRoundedDefault; 790 int sizeTop = mRoundedDefaultTop; 791 int sizeBottom = mRoundedDefaultBottom; 792 if (newValue != null) { 793 try { 794 size = (int) (Integer.parseInt(newValue) * mDensity); 795 } catch (Exception e) { 796 } 797 } 798 799 if (sizeTop == 0) { 800 sizeTop = size; 801 } 802 if (sizeBottom == 0) { 803 sizeBottom = size; 804 } 805 806 setSize(mOverlay.findViewById(R.id.left), sizeTop); 807 setSize(mOverlay.findViewById(R.id.right), sizeTop); 808 setSize(mBottomOverlay.findViewById(R.id.left), sizeBottom); 809 setSize(mBottomOverlay.findViewById(R.id.right), sizeBottom); 810 } 811 }); 812 } 813 setSize(View view, int pixelSize)814 private void setSize(View view, int pixelSize) { 815 LayoutParams params = view.getLayoutParams(); 816 params.width = pixelSize; 817 params.height = pixelSize; 818 view.setLayoutParams(params); 819 } 820 821 @Override onDarkIntensity(float darkIntensity)822 public void onDarkIntensity(float darkIntensity) { 823 if (!mHandler.getLooper().isCurrentThread()) { 824 mHandler.post(() -> onDarkIntensity(darkIntensity)); 825 return; 826 } 827 if (mOverlay != null) { 828 CornerHandleView assistHintTopLeft = mOverlay.findViewById(R.id.assist_hint_left); 829 CornerHandleView assistHintTopRight = mOverlay.findViewById(R.id.assist_hint_right); 830 831 assistHintTopLeft.updateDarkness(darkIntensity); 832 assistHintTopRight.updateDarkness(darkIntensity); 833 } 834 835 if (mBottomOverlay != null) { 836 CornerHandleView assistHintBottomLeft = mBottomOverlay.findViewById( 837 R.id.assist_hint_left); 838 CornerHandleView assistHintBottomRight = mBottomOverlay.findViewById( 839 R.id.assist_hint_right); 840 841 assistHintBottomLeft.updateDarkness(darkIntensity); 842 assistHintBottomRight.updateDarkness(darkIntensity); 843 } 844 } 845 846 @VisibleForTesting 847 static class TunablePaddingTagListener implements FragmentListener { 848 849 private final int mPadding; 850 private final int mId; 851 private TunablePadding mTunablePadding; 852 TunablePaddingTagListener(int padding, int id)853 public TunablePaddingTagListener(int padding, int id) { 854 mPadding = padding; 855 mId = id; 856 } 857 858 @Override onFragmentViewCreated(String tag, Fragment fragment)859 public void onFragmentViewCreated(String tag, Fragment fragment) { 860 if (mTunablePadding != null) { 861 mTunablePadding.destroy(); 862 } 863 View view = fragment.getView(); 864 if (mId != 0) { 865 view = view.findViewById(mId); 866 } 867 mTunablePadding = TunablePadding.addTunablePadding(view, PADDING, mPadding, 868 FLAG_START | FLAG_END); 869 } 870 } 871 872 public static class DisplayCutoutView extends View implements DisplayManager.DisplayListener, 873 RegionInterceptableView { 874 875 private static final float HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f; 876 877 private final DisplayInfo mInfo = new DisplayInfo(); 878 private final Paint mPaint = new Paint(); 879 private final List<Rect> mBounds = new ArrayList(); 880 private final Rect mBoundingRect = new Rect(); 881 private final Path mBoundingPath = new Path(); 882 // Don't initialize these yet because they may never exist 883 private RectF mProtectionRect; 884 private RectF mProtectionRectOrig; 885 private Path mProtectionPath; 886 private Path mProtectionPathOrig; 887 private Rect mTotalBounds = new Rect(); 888 // Whether or not to show the cutout protection path 889 private boolean mShowProtection = false; 890 891 private final int[] mLocation = new int[2]; 892 private final boolean mInitialStart; 893 private final Runnable mVisibilityChangedListener; 894 private final ScreenDecorations mDecorations; 895 private int mColor = Color.BLACK; 896 private boolean mStart; 897 private int mRotation; 898 private float mCameraProtectionProgress = HIDDEN_CAMERA_PROTECTION_SCALE; 899 private ValueAnimator mCameraProtectionAnimator; 900 DisplayCutoutView(Context context, boolean start, Runnable visibilityChangedListener, ScreenDecorations decorations)901 public DisplayCutoutView(Context context, boolean start, 902 Runnable visibilityChangedListener, ScreenDecorations decorations) { 903 super(context); 904 mInitialStart = start; 905 mVisibilityChangedListener = visibilityChangedListener; 906 mDecorations = decorations; 907 setId(R.id.display_cutout); 908 if (DEBUG) { 909 getViewTreeObserver().addOnDrawListener(() -> Log.i(TAG, 910 (mInitialStart ? "OverlayTop" : "OverlayBottom") 911 + " drawn in rot " + mRotation)); 912 } 913 } 914 setColor(int color)915 public void setColor(int color) { 916 mColor = color; 917 invalidate(); 918 } 919 920 @Override onAttachedToWindow()921 protected void onAttachedToWindow() { 922 super.onAttachedToWindow(); 923 mContext.getSystemService(DisplayManager.class).registerDisplayListener(this, 924 getHandler()); 925 update(); 926 } 927 928 @Override onDetachedFromWindow()929 protected void onDetachedFromWindow() { 930 super.onDetachedFromWindow(); 931 mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this); 932 } 933 934 @Override onDraw(Canvas canvas)935 protected void onDraw(Canvas canvas) { 936 super.onDraw(canvas); 937 getLocationOnScreen(mLocation); 938 canvas.translate(-mLocation[0], -mLocation[1]); 939 940 if (!mBoundingPath.isEmpty()) { 941 mPaint.setColor(mColor); 942 mPaint.setStyle(Paint.Style.FILL); 943 mPaint.setAntiAlias(true); 944 canvas.drawPath(mBoundingPath, mPaint); 945 } 946 if (mCameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE 947 && !mProtectionRect.isEmpty()) { 948 canvas.scale(mCameraProtectionProgress, mCameraProtectionProgress, 949 mProtectionRect.centerX(), mProtectionRect.centerY()); 950 canvas.drawPath(mProtectionPath, mPaint); 951 } 952 } 953 954 @Override onDisplayAdded(int displayId)955 public void onDisplayAdded(int displayId) { 956 } 957 958 @Override onDisplayRemoved(int displayId)959 public void onDisplayRemoved(int displayId) { 960 } 961 962 @Override onDisplayChanged(int displayId)963 public void onDisplayChanged(int displayId) { 964 if (displayId == getDisplay().getDisplayId()) { 965 update(); 966 } 967 } 968 setRotation(int rotation)969 public void setRotation(int rotation) { 970 mRotation = rotation; 971 update(); 972 } 973 setProtection(Path protectionPath, Rect pathBounds)974 void setProtection(Path protectionPath, Rect pathBounds) { 975 if (mProtectionPathOrig == null) { 976 mProtectionPathOrig = new Path(); 977 mProtectionPath = new Path(); 978 } 979 mProtectionPathOrig.set(protectionPath); 980 if (mProtectionRectOrig == null) { 981 mProtectionRectOrig = new RectF(); 982 mProtectionRect = new RectF(); 983 } 984 mProtectionRectOrig.set(pathBounds); 985 } 986 setShowProtection(boolean shouldShow)987 void setShowProtection(boolean shouldShow) { 988 if (mShowProtection == shouldShow) { 989 return; 990 } 991 992 mShowProtection = shouldShow; 993 updateBoundingPath(); 994 // Delay the relayout until the end of the animation when hiding the cutout, 995 // otherwise we'd clip it. 996 if (mShowProtection) { 997 requestLayout(); 998 } 999 if (mCameraProtectionAnimator != null) { 1000 mCameraProtectionAnimator.cancel(); 1001 } 1002 mCameraProtectionAnimator = ValueAnimator.ofFloat(mCameraProtectionProgress, 1003 mShowProtection ? 1.0f : HIDDEN_CAMERA_PROTECTION_SCALE).setDuration(750); 1004 mCameraProtectionAnimator.setInterpolator(Interpolators.DECELERATE_QUINT); 1005 mCameraProtectionAnimator.addUpdateListener(animation -> { 1006 mCameraProtectionProgress = (float) animation.getAnimatedValue(); 1007 invalidate(); 1008 }); 1009 mCameraProtectionAnimator.addListener(new AnimatorListenerAdapter() { 1010 @Override 1011 public void onAnimationEnd(Animator animation) { 1012 mCameraProtectionAnimator = null; 1013 if (!mShowProtection) { 1014 requestLayout(); 1015 } 1016 } 1017 }); 1018 mCameraProtectionAnimator.start(); 1019 } 1020 isStart()1021 private boolean isStart() { 1022 final boolean flipped = (mRotation == RotationUtils.ROTATION_SEASCAPE 1023 || mRotation == RotationUtils.ROTATION_UPSIDE_DOWN); 1024 return flipped ? !mInitialStart : mInitialStart; 1025 } 1026 update()1027 private void update() { 1028 if (!isAttachedToWindow() || mDecorations.mPendingRotationChange) { 1029 return; 1030 } 1031 mStart = isStart(); 1032 requestLayout(); 1033 getDisplay().getDisplayInfo(mInfo); 1034 mBounds.clear(); 1035 mBoundingRect.setEmpty(); 1036 mBoundingPath.reset(); 1037 int newVisible; 1038 if (shouldDrawCutout(getContext()) && hasCutout()) { 1039 mBounds.addAll(mInfo.displayCutout.getBoundingRects()); 1040 localBounds(mBoundingRect); 1041 updateGravity(); 1042 updateBoundingPath(); 1043 invalidate(); 1044 newVisible = VISIBLE; 1045 } else { 1046 newVisible = GONE; 1047 } 1048 if (newVisible != getVisibility()) { 1049 setVisibility(newVisible); 1050 mVisibilityChangedListener.run(); 1051 } 1052 } 1053 updateBoundingPath()1054 private void updateBoundingPath() { 1055 int lw = mInfo.logicalWidth; 1056 int lh = mInfo.logicalHeight; 1057 1058 boolean flipped = mInfo.rotation == ROTATION_90 || mInfo.rotation == ROTATION_270; 1059 1060 int dw = flipped ? lh : lw; 1061 int dh = flipped ? lw : lh; 1062 1063 mBoundingPath.set(DisplayCutout.pathFromResources(getResources(), dw, dh)); 1064 Matrix m = new Matrix(); 1065 transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m); 1066 mBoundingPath.transform(m); 1067 if (mProtectionPathOrig != null) { 1068 // Reset the protection path so we don't aggregate rotations 1069 mProtectionPath.set(mProtectionPathOrig); 1070 mProtectionPath.transform(m); 1071 m.mapRect(mProtectionRect, mProtectionRectOrig); 1072 } 1073 } 1074 transformPhysicalToLogicalCoordinates(@urface.Rotation int rotation, @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out)1075 private static void transformPhysicalToLogicalCoordinates(@Surface.Rotation int rotation, 1076 @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) { 1077 switch (rotation) { 1078 case ROTATION_0: 1079 out.reset(); 1080 break; 1081 case ROTATION_90: 1082 out.setRotate(270); 1083 out.postTranslate(0, physicalWidth); 1084 break; 1085 case ROTATION_180: 1086 out.setRotate(180); 1087 out.postTranslate(physicalWidth, physicalHeight); 1088 break; 1089 case ROTATION_270: 1090 out.setRotate(90); 1091 out.postTranslate(physicalHeight, 0); 1092 break; 1093 default: 1094 throw new IllegalArgumentException("Unknown rotation: " + rotation); 1095 } 1096 } 1097 updateGravity()1098 private void updateGravity() { 1099 LayoutParams lp = getLayoutParams(); 1100 if (lp instanceof FrameLayout.LayoutParams) { 1101 FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) lp; 1102 int newGravity = getGravity(mInfo.displayCutout); 1103 if (flp.gravity != newGravity) { 1104 flp.gravity = newGravity; 1105 setLayoutParams(flp); 1106 } 1107 } 1108 } 1109 hasCutout()1110 private boolean hasCutout() { 1111 final DisplayCutout displayCutout = mInfo.displayCutout; 1112 if (displayCutout == null) { 1113 return false; 1114 } 1115 if (mStart) { 1116 return displayCutout.getSafeInsetLeft() > 0 1117 || displayCutout.getSafeInsetTop() > 0; 1118 } else { 1119 return displayCutout.getSafeInsetRight() > 0 1120 || displayCutout.getSafeInsetBottom() > 0; 1121 } 1122 } 1123 1124 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)1125 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 1126 if (mBounds.isEmpty()) { 1127 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 1128 return; 1129 } 1130 1131 if (mShowProtection) { 1132 // Make sure that our measured height encompases the protection 1133 mTotalBounds.union(mBoundingRect); 1134 mTotalBounds.union((int) mProtectionRect.left, (int) mProtectionRect.top, 1135 (int) mProtectionRect.right, (int) mProtectionRect.bottom); 1136 setMeasuredDimension( 1137 resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0), 1138 resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0)); 1139 } else { 1140 setMeasuredDimension( 1141 resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0), 1142 resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0)); 1143 } 1144 } 1145 boundsFromDirection(DisplayCutout displayCutout, int gravity, Rect out)1146 public static void boundsFromDirection(DisplayCutout displayCutout, int gravity, 1147 Rect out) { 1148 switch (gravity) { 1149 case Gravity.TOP: 1150 out.set(displayCutout.getBoundingRectTop()); 1151 break; 1152 case Gravity.LEFT: 1153 out.set(displayCutout.getBoundingRectLeft()); 1154 break; 1155 case Gravity.BOTTOM: 1156 out.set(displayCutout.getBoundingRectBottom()); 1157 break; 1158 case Gravity.RIGHT: 1159 out.set(displayCutout.getBoundingRectRight()); 1160 break; 1161 default: 1162 out.setEmpty(); 1163 } 1164 } 1165 localBounds(Rect out)1166 private void localBounds(Rect out) { 1167 DisplayCutout displayCutout = mInfo.displayCutout; 1168 boundsFromDirection(displayCutout, getGravity(displayCutout), out); 1169 } 1170 getGravity(DisplayCutout displayCutout)1171 private int getGravity(DisplayCutout displayCutout) { 1172 if (mStart) { 1173 if (displayCutout.getSafeInsetLeft() > 0) { 1174 return Gravity.LEFT; 1175 } else if (displayCutout.getSafeInsetTop() > 0) { 1176 return Gravity.TOP; 1177 } 1178 } else { 1179 if (displayCutout.getSafeInsetRight() > 0) { 1180 return Gravity.RIGHT; 1181 } else if (displayCutout.getSafeInsetBottom() > 0) { 1182 return Gravity.BOTTOM; 1183 } 1184 } 1185 return Gravity.NO_GRAVITY; 1186 } 1187 1188 @Override shouldInterceptTouch()1189 public boolean shouldInterceptTouch() { 1190 return mInfo.displayCutout != null && getVisibility() == VISIBLE; 1191 } 1192 1193 @Override getInterceptRegion()1194 public Region getInterceptRegion() { 1195 if (mInfo.displayCutout == null) { 1196 return null; 1197 } 1198 1199 View rootView = getRootView(); 1200 Region cutoutBounds = rectsToRegion( 1201 mInfo.displayCutout.getBoundingRects()); 1202 1203 // Transform to window's coordinate space 1204 rootView.getLocationOnScreen(mLocation); 1205 cutoutBounds.translate(-mLocation[0], -mLocation[1]); 1206 1207 // Intersect with window's frame 1208 cutoutBounds.op(rootView.getLeft(), rootView.getTop(), rootView.getRight(), 1209 rootView.getBottom(), Region.Op.INTERSECT); 1210 1211 return cutoutBounds; 1212 } 1213 } 1214 isLandscape(int rotation)1215 private boolean isLandscape(int rotation) { 1216 return rotation == RotationUtils.ROTATION_LANDSCAPE || rotation == 1217 RotationUtils.ROTATION_SEASCAPE; 1218 } 1219 1220 /** 1221 * A pre-draw listener, that cancels the draw and restarts the traversal with the updated 1222 * window attributes. 1223 */ 1224 private class RestartingPreDrawListener implements ViewTreeObserver.OnPreDrawListener { 1225 1226 private final View mView; 1227 private final int mTargetRotation; 1228 RestartingPreDrawListener(View view, int targetRotation)1229 private RestartingPreDrawListener(View view, int targetRotation) { 1230 mView = view; 1231 mTargetRotation = targetRotation; 1232 } 1233 1234 @Override onPreDraw()1235 public boolean onPreDraw() { 1236 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1237 1238 if (mTargetRotation == mRotation) { 1239 if (DEBUG) { 1240 Log.i(TAG, (mView == mOverlay ? "OverlayTop" : "OverlayBottom") 1241 + " already in target rot " 1242 + mTargetRotation + ", allow draw without restarting it"); 1243 } 1244 return true; 1245 } 1246 1247 mPendingRotationChange = false; 1248 // This changes the window attributes - we need to restart the traversal for them to 1249 // take effect. 1250 updateOrientation(); 1251 if (DEBUG) { 1252 Log.i(TAG, (mView == mOverlay ? "OverlayTop" : "OverlayBottom") 1253 + " restarting listener fired, restarting draw for rot " + mRotation); 1254 } 1255 mView.invalidate(); 1256 return false; 1257 } 1258 } 1259 1260 /** 1261 * A pre-draw listener, that validates that the rotation we draw in matches the displays 1262 * rotation before continuing the draw. 1263 * 1264 * This is to prevent a race condition, where we have not received the display changed event 1265 * yet, and would thus draw in an old orientation. 1266 */ 1267 private class ValidatingPreDrawListener implements ViewTreeObserver.OnPreDrawListener { 1268 1269 private final View mView; 1270 ValidatingPreDrawListener(View view)1271 public ValidatingPreDrawListener(View view) { 1272 mView = view; 1273 } 1274 1275 @Override onPreDraw()1276 public boolean onPreDraw() { 1277 final int displayRotation = RotationUtils.getExactRotation(mContext); 1278 if (displayRotation != mRotation && !mPendingRotationChange) { 1279 if (DEBUG) { 1280 Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot " 1281 + displayRotation + ". Restarting draw"); 1282 } 1283 mView.invalidate(); 1284 return false; 1285 } 1286 return true; 1287 } 1288 } 1289 } 1290