blob: 2c0b52638d2f2c46fc3aa707d580a0378576b7e4 [file] [log] [blame]
/*
* Copyright (C) 2017 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package com.android.systemui;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.DisplayCutout.BOUNDS_POSITION_BOTTOM;
import static android.view.DisplayCutout.BOUNDS_POSITION_LEFT;
import static android.view.DisplayCutout.BOUNDS_POSITION_LENGTH;
import static android.view.DisplayCutout.BOUNDS_POSITION_RIGHT;
import static android.view.DisplayCutout.BOUNDS_POSITION_TOP;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.annotation.Dimension;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.os.Handler;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.provider.Settings.Secure;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.DisplayCutout.BoundsPosition;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.systemui.RegionInterceptingFrameLayout.RegionInterceptableView;
import com.android.systemui.animation.Interpolators;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.qs.SecureSetting;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.events.PrivacyDotViewController;
import com.android.systemui.tuner.TunerService;
import com.android.systemui.tuner.TunerService.Tunable;
import com.android.systemui.util.concurrency.DelayableExecutor;
import com.android.systemui.util.concurrency.ThreadFactory;
import com.android.systemui.util.settings.SecureSettings;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import javax.inject.Inject;
/**
* An overlay that draws screen decorations in software (e.g for rounded corners or display cutout)
* for antialiasing and emulation purposes.
*/
@SysUISingleton
public class ScreenDecorations extends SystemUI implements Tunable {
private static final boolean DEBUG = false;
private static final String TAG = "ScreenDecorations";
public static final String SIZE = "sysui_rounded_size";
public static final String PADDING = "sysui_rounded_content_padding";
// Provide a way for factory to disable ScreenDecorations to run the Display tests.
private static final boolean DEBUG_DISABLE_SCREEN_DECORATIONS =
SystemProperties.getBoolean("debug.disable_screen_decorations", false);
private static final boolean DEBUG_SCREENSHOT_ROUNDED_CORNERS =
SystemProperties.getBoolean("debug.screenshot_rounded_corners", false);
private static final boolean VERBOSE = false;
private static final boolean DEBUG_COLOR = DEBUG_SCREENSHOT_ROUNDED_CORNERS;
private DisplayManager mDisplayManager;
@VisibleForTesting
protected boolean mIsRegistered;
private final BroadcastDispatcher mBroadcastDispatcher;
private final Executor mMainExecutor;
private final TunerService mTunerService;
private final SecureSettings mSecureSettings;
private DisplayManager.DisplayListener mDisplayListener;
private CameraAvailabilityListener mCameraListener;
private final UserTracker mUserTracker;
private final PrivacyDotViewController mDotViewController;
private final ThreadFactory mThreadFactory;
//TODO: These are piecemeal being updated to Points for now to support non-square rounded
// corners. for now it is only supposed when reading the intrinsic size from the drawables with
// mIsRoundedCornerMultipleRadius is set
@VisibleForTesting
protected Point mRoundedDefault = new Point(0, 0);
@VisibleForTesting
protected Point mRoundedDefaultTop = new Point(0, 0);
@VisibleForTesting
protected Point mRoundedDefaultBottom = new Point(0, 0);
@VisibleForTesting
protected View[] mOverlays;
@Nullable
private DisplayCutoutView[] mCutoutViews;
//TODO:
View mTopLeftDot;
View mTopRightDot;
View mBottomLeftDot;
View mBottomRightDot;
private float mDensity;
private WindowManager mWindowManager;
private int mRotation;
private SecureSetting mColorInversionSetting;
private DelayableExecutor mExecutor;
private Handler mHandler;
private boolean mPendingRotationChange;
private boolean mIsRoundedCornerMultipleRadius;
private int mStatusBarHeightPortrait;
private int mStatusBarHeightLandscape;
private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback =
new CameraAvailabilityListener.CameraTransitionCallback() {
@Override
public void onApplyCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
if (mCutoutViews == null) {
Log.w(TAG, "DisplayCutoutView do not initialized");
return;
}
// Show the extra protection around the front facing camera if necessary
for (DisplayCutoutView dcv : mCutoutViews) {
// Check Null since not all mCutoutViews[pos] be inflated at the meanwhile
if (dcv != null) {
dcv.setProtection(protectionPath, bounds);
dcv.setShowProtection(true);
}
}
}
@Override
public void onHideCameraProtection() {
if (mCutoutViews == null) {
Log.w(TAG, "DisplayCutoutView do not initialized");
return;
}
// Go back to the regular anti-aliasing
for (DisplayCutoutView dcv : mCutoutViews) {
// Check Null since not all mCutoutViews[pos] be inflated at the meanwhile
if (dcv != null) {
dcv.setShowProtection(false);
}
}
}
};
/**
* Converts a set of {@link Rect}s into a {@link Region}
*
* @hide
*/
public static Region rectsToRegion(List<Rect> rects) {
Region result = Region.obtain();
if (rects != null) {
for (Rect r : rects) {
if (r != null && !r.isEmpty()) {
result.op(r, Region.Op.UNION);
}
}
}
return result;
}
@Inject
public ScreenDecorations(Context context,
@Main Executor mainExecutor,
SecureSettings secureSettings,
BroadcastDispatcher broadcastDispatcher,
TunerService tunerService,
UserTracker userTracker,
PrivacyDotViewController dotViewController,
ThreadFactory threadFactory) {
super(context);
mMainExecutor = mainExecutor;
mSecureSettings = secureSettings;
mBroadcastDispatcher = broadcastDispatcher;
mTunerService = tunerService;
mUserTracker = userTracker;
mDotViewController = dotViewController;
mThreadFactory = threadFactory;
}
@Override
public void start() {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
Log.i(TAG, "ScreenDecorations is disabled");
return;
}
mHandler = mThreadFactory.buildHandlerOnNewThread("ScreenDecorations");
mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler);
mExecutor.execute(this::startOnScreenDecorationsThread);
mDotViewController.setUiExecutor(mExecutor);
}
private void startOnScreenDecorationsThread() {
mRotation = mContext.getDisplay().getRotation();
mWindowManager = mContext.getSystemService(WindowManager.class);
mDisplayManager = mContext.getSystemService(DisplayManager.class);
mIsRoundedCornerMultipleRadius = mContext.getResources().getBoolean(
R.bool.config_roundedCornerMultipleRadius);
updateRoundedCornerRadii();
setupDecorations();
setupCameraListener();
mDisplayListener = new DisplayManager.DisplayListener() {
@Override
public void onDisplayAdded(int displayId) {
// do nothing
}
@Override
public void onDisplayRemoved(int displayId) {
// do nothing
}
@Override
public void onDisplayChanged(int displayId) {
final int newRotation = mContext.getDisplay().getRotation();
if (mOverlays != null && mRotation != newRotation) {
// We cannot immediately update the orientation. Otherwise
// WindowManager is still deferring layout until it has finished dispatching
// the config changes, which may cause divergence between what we draw
// (new orientation), and where we are placed on the screen (old orientation).
// Instead we wait until either:
// - we are trying to redraw. This because WM resized our window and told us to.
// - the config change has been dispatched, so WM is no longer deferring layout.
mPendingRotationChange = true;
if (DEBUG) {
Log.i(TAG, "Rotation changed, deferring " + newRotation + ", staying at "
+ mRotation);
}
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] != null) {
mOverlays[i].getViewTreeObserver().addOnPreDrawListener(
new RestartingPreDrawListener(mOverlays[i], i, newRotation));
}
}
}
updateOrientation();
}
};
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
updateOrientation();
}
private void setupDecorations() {
if (hasRoundedCorners() || shouldDrawCutout()) {
updateStatusBarHeight();
final DisplayCutout cutout = getCutout();
final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll();
int rotatedPos;
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
rotatedPos = getBoundPositionFromRotation(i, mRotation);
if ((bounds != null && !bounds[rotatedPos].isEmpty())
|| shouldShowRoundedCorner(i)) {
createOverlay(i);
} else {
removeOverlay(i);
}
}
// Overlays have been created, send the dots to the controller
//TODO: need a better way to do this
mDotViewController.initialize(
mTopLeftDot, mTopRightDot, mBottomLeftDot, mBottomRightDot);
} else {
removeAllOverlays();
}
if (hasOverlays()) {
if (mIsRegistered) {
return;
}
DisplayMetrics metrics = new DisplayMetrics();
mDisplayManager.getDisplay(DEFAULT_DISPLAY).getMetrics(metrics);
mDensity = metrics.density;
mExecutor.execute(() -> mTunerService.addTunable(this, SIZE));
// Watch color inversion and invert the overlay as needed.
if (mColorInversionSetting == null) {
mColorInversionSetting = new SecureSetting(mSecureSettings, mHandler,
Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
mUserTracker.getUserId()) {
@Override
protected void handleValueChanged(int value, boolean observedChange) {
updateColorInversion(value);
}
};
}
mColorInversionSetting.setListening(true);
mColorInversionSetting.onChange(false);
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_SWITCHED);
mBroadcastDispatcher.registerReceiver(mUserSwitchIntentReceiver, filter,
mExecutor, UserHandle.ALL);
mIsRegistered = true;
} else {
mMainExecutor.execute(() -> mTunerService.removeTunable(this));
if (mColorInversionSetting != null) {
mColorInversionSetting.setListening(false);
}
mBroadcastDispatcher.unregisterReceiver(mUserSwitchIntentReceiver);
mIsRegistered = false;
}
}
@VisibleForTesting
DisplayCutout getCutout() {
return mContext.getDisplay().getCutout();
}
@VisibleForTesting
boolean hasOverlays() {
if (mOverlays == null) {
return false;
}
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] != null) {
return true;
}
}
mOverlays = null;
return false;
}
private void removeAllOverlays() {
if (mOverlays == null) {
return;
}
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] != null) {
removeOverlay(i);
}
}
mOverlays = null;
}
private void removeOverlay(@BoundsPosition int pos) {
if (mOverlays == null || mOverlays[pos] == null) {
return;
}
mWindowManager.removeViewImmediate(mOverlays[pos]);
mOverlays[pos] = null;
}
private void createOverlay(@BoundsPosition int pos) {
if (mOverlays == null) {
mOverlays = new View[BOUNDS_POSITION_LENGTH];
}
if (mCutoutViews == null) {
mCutoutViews = new DisplayCutoutView[BOUNDS_POSITION_LENGTH];
}
if (mOverlays[pos] != null) {
return;
}
mOverlays[pos] = overlayForPosition(pos);
mCutoutViews[pos] = new DisplayCutoutView(mContext, pos, this);
((ViewGroup) mOverlays[pos]).addView(mCutoutViews[pos]);
mOverlays[pos].setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
mOverlays[pos].setAlpha(0);
mOverlays[pos].setForceDarkAllowed(false);
updateView(pos);
mWindowManager.addView(mOverlays[pos], getWindowLayoutParams(pos));
mOverlays[pos].addOnLayoutChangeListener(new OnLayoutChangeListener() {
@Override
public void onLayoutChange(View v, int left, int top, int right, int bottom,
int oldLeft, int oldTop, int oldRight, int oldBottom) {
mOverlays[pos].removeOnLayoutChangeListener(this);
mOverlays[pos].animate()
.alpha(1)
.setDuration(1000)
.start();
}
});
mOverlays[pos].getViewTreeObserver().addOnPreDrawListener(
new ValidatingPreDrawListener(mOverlays[pos]));
}
/**
* Allow overrides for top/bottom positions
*/
private View overlayForPosition(@BoundsPosition int pos) {
switch (pos) {
case BOUNDS_POSITION_TOP:
case BOUNDS_POSITION_LEFT:
View top = LayoutInflater.from(mContext)
.inflate(R.layout.rounded_corners_top, null);
mTopLeftDot = top.findViewById(R.id.privacy_dot_left_container);
mTopRightDot = top.findViewById(R.id.privacy_dot_right_container);
return top;
case BOUNDS_POSITION_BOTTOM:
case BOUNDS_POSITION_RIGHT:
View bottom = LayoutInflater.from(mContext)
.inflate(R.layout.rounded_corners_bottom, null);
mBottomLeftDot = bottom.findViewById(R.id.privacy_dot_left_container);
mBottomRightDot = bottom.findViewById(R.id.privacy_dot_right_container);
return bottom;
default:
throw new IllegalArgumentException("Unknown bounds position");
}
}
private void updateView(@BoundsPosition int pos) {
if (mOverlays == null || mOverlays[pos] == null) {
return;
}
// update rounded corner view rotation
updateRoundedCornerView(pos, R.id.left);
updateRoundedCornerView(pos, R.id.right);
updateRoundedCornerSize(mRoundedDefault, mRoundedDefaultTop, mRoundedDefaultBottom);
// update cutout view rotation
if (mCutoutViews != null && mCutoutViews[pos] != null) {
mCutoutViews[pos].setRotation(mRotation);
}
}
@VisibleForTesting
WindowManager.LayoutParams getWindowLayoutParams(@BoundsPosition int pos) {
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
getWidthLayoutParamByPos(pos),
getHeightLayoutParamByPos(pos),
WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
| WindowManager.LayoutParams.FLAG_SLIPPERY
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
PixelFormat.TRANSLUCENT);
lp.privateFlags |= WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS
| WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION;
// FLAG_SLIPPERY can only be set by trusted overlays
lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
if (!DEBUG_SCREENSHOT_ROUNDED_CORNERS) {
lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
}
lp.setTitle(getWindowTitleByPos(pos));
lp.gravity = getOverlayWindowGravity(pos);
lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
lp.setFitInsetsTypes(0 /* types */);
lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
return lp;
}
private int getWidthLayoutParamByPos(@BoundsPosition int pos) {
final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
return rotatedPos == BOUNDS_POSITION_TOP || rotatedPos == BOUNDS_POSITION_BOTTOM
? MATCH_PARENT : WRAP_CONTENT;
}
private int getHeightLayoutParamByPos(@BoundsPosition int pos) {
final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
return rotatedPos == BOUNDS_POSITION_TOP || rotatedPos == BOUNDS_POSITION_BOTTOM
? WRAP_CONTENT : MATCH_PARENT;
}
private static String getWindowTitleByPos(@BoundsPosition int pos) {
switch (pos) {
case BOUNDS_POSITION_LEFT:
return "ScreenDecorOverlayLeft";
case BOUNDS_POSITION_TOP:
return "ScreenDecorOverlay";
case BOUNDS_POSITION_RIGHT:
return "ScreenDecorOverlayRight";
case BOUNDS_POSITION_BOTTOM:
return "ScreenDecorOverlayBottom";
default:
throw new IllegalArgumentException("unknown bound position: " + pos);
}
}
private int getOverlayWindowGravity(@BoundsPosition int pos) {
final int rotated = getBoundPositionFromRotation(pos, mRotation);
switch (rotated) {
case BOUNDS_POSITION_TOP:
return Gravity.TOP;
case BOUNDS_POSITION_BOTTOM:
return Gravity.BOTTOM;
case BOUNDS_POSITION_LEFT:
return Gravity.LEFT;
case BOUNDS_POSITION_RIGHT:
return Gravity.RIGHT;
default:
throw new IllegalArgumentException("unknown bound position: " + pos);
}
}
private static int getBoundPositionFromRotation(@BoundsPosition int pos, int rotation) {
return (pos - rotation) < 0
? pos - rotation + DisplayCutout.BOUNDS_POSITION_LENGTH
: pos - rotation;
}
private void setupCameraListener() {
Resources res = mContext.getResources();
boolean enabled = res.getBoolean(R.bool.config_enableDisplayCutoutProtection);
if (enabled) {
mCameraListener = CameraAvailabilityListener.Factory.build(mContext, mExecutor);
mCameraListener.addTransitionCallback(mCameraTransitionCallback);
mCameraListener.startListening();
}
}
private final BroadcastReceiver mUserSwitchIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int newUserId = ActivityManager.getCurrentUser();
if (DEBUG) {
Log.d(TAG, "UserSwitched newUserId=" + newUserId);
}
// update color inversion setting to the new user
mColorInversionSetting.setUserId(newUserId);
updateColorInversion(mColorInversionSetting.getValue());
}
};
private void updateColorInversion(int colorsInvertedValue) {
int tint = colorsInvertedValue != 0 ? Color.WHITE : Color.BLACK;
if (DEBUG_COLOR) {
tint = Color.RED;
}
ColorStateList tintList = ColorStateList.valueOf(tint);
if (mOverlays == null) {
return;
}
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] == null) {
continue;
}
final int size = ((ViewGroup) mOverlays[i]).getChildCount();
View child;
for (int j = 0; j < size; j++) {
child = ((ViewGroup) mOverlays[i]).getChildAt(j);
if (child.getId() == R.id.privacy_dot_left_container
|| child.getId() == R.id.privacy_dot_right_container) {
// Exclude privacy dot from color inversion (for now?)
continue;
}
if (child instanceof ImageView) {
((ImageView) child).setImageTintList(tintList);
} else if (child instanceof DisplayCutoutView) {
((DisplayCutoutView) child).setColor(tint);
}
}
}
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
Log.i(TAG, "ScreenDecorations is disabled");
return;
}
mExecutor.execute(() -> {
int oldRotation = mRotation;
mPendingRotationChange = false;
updateOrientation();
updateRoundedCornerRadii();
if (DEBUG) Log.i(TAG, "onConfigChanged from rot " + oldRotation + " to " + mRotation);
setupDecorations();
if (mOverlays != null) {
// Updating the layout params ensures that ViewRootImpl will call relayoutWindow(),
// which ensures that the forced seamless rotation will end, even if we updated
// the rotation before window manager was ready (and was still waiting for sending
// the updated rotation).
updateLayoutParams();
}
});
}
private void updateOrientation() {
Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(),
"must call on " + mHandler.getLooper().getThread()
+ ", but was " + Thread.currentThread());
int newRotation = mContext.getDisplay().getRotation();
if (mRotation != newRotation) {
mDotViewController.setNewRotation(newRotation);
}
if (mPendingRotationChange) {
return;
}
if (newRotation != mRotation) {
mRotation = newRotation;
if (mOverlays != null) {
updateLayoutParams();
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] == null) {
continue;
}
updateView(i);
}
}
}
}
private void updateStatusBarHeight() {
mStatusBarHeightLandscape = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.status_bar_height_landscape);
mStatusBarHeightPortrait = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.status_bar_height_portrait);
mDotViewController.setStatusBarHeights(mStatusBarHeightPortrait, mStatusBarHeightLandscape);
}
private void updateRoundedCornerRadii() {
// We should eventually move to just using the intrinsic size of the drawables since
// they should be sized to the exact pixels they want to cover. Therefore I'm purposely not
// upgrading all of the configs to contain (width, height) pairs. Instead assume that a
// device configured using the single integer config value is okay with drawing the corners
// as a square
final int newRoundedDefault = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.rounded_corner_radius);
final int newRoundedDefaultTop = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.rounded_corner_radius_top);
final int newRoundedDefaultBottom = mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.rounded_corner_radius_bottom);
final boolean changed = mRoundedDefault.x != newRoundedDefault
|| mRoundedDefaultTop.x != newRoundedDefaultTop
|| mRoundedDefaultBottom.x != newRoundedDefaultBottom;
if (changed) {
// If config_roundedCornerMultipleRadius set as true, ScreenDecorations respect the
// (width, height) size of drawable/rounded.xml instead of rounded_corner_radius
if (mIsRoundedCornerMultipleRadius) {
Drawable d = mContext.getDrawable(R.drawable.rounded);
mRoundedDefault.set(d.getIntrinsicWidth(), d.getIntrinsicHeight());
d = mContext.getDrawable(R.drawable.rounded_corner_top);
mRoundedDefaultTop.set(d.getIntrinsicWidth(), d.getIntrinsicHeight());
d = mContext.getDrawable(R.drawable.rounded_corner_bottom);
mRoundedDefaultBottom.set(d.getIntrinsicWidth(), d.getIntrinsicHeight());
} else {
mRoundedDefault.set(newRoundedDefault, newRoundedDefault);
mRoundedDefaultTop.set(newRoundedDefaultTop, newRoundedDefaultTop);
mRoundedDefaultBottom.set(newRoundedDefaultBottom, newRoundedDefaultBottom);
}
onTuningChanged(SIZE, null);
}
}
private void updateRoundedCornerView(@BoundsPosition int pos, int id) {
final View rounded = mOverlays[pos].findViewById(id);
if (rounded == null) {
return;
}
rounded.setVisibility(View.GONE);
if (shouldShowRoundedCorner(pos)) {
final int gravity = getRoundedCornerGravity(pos, id == R.id.left);
((FrameLayout.LayoutParams) rounded.getLayoutParams()).gravity = gravity;
setRoundedCornerOrientation(rounded, gravity);
rounded.setVisibility(View.VISIBLE);
}
}
private int getRoundedCornerGravity(@BoundsPosition int pos, boolean isStart) {
final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
switch (rotatedPos) {
case BOUNDS_POSITION_LEFT:
return isStart ? Gravity.TOP | Gravity.LEFT : Gravity.BOTTOM | Gravity.LEFT;
case BOUNDS_POSITION_TOP:
return isStart ? Gravity.TOP | Gravity.LEFT : Gravity.TOP | Gravity.RIGHT;
case BOUNDS_POSITION_RIGHT:
return isStart ? Gravity.TOP | Gravity.RIGHT : Gravity.BOTTOM | Gravity.RIGHT;
case BOUNDS_POSITION_BOTTOM:
return isStart ? Gravity.BOTTOM | Gravity.LEFT : Gravity.BOTTOM | Gravity.RIGHT;
default:
throw new IllegalArgumentException("Incorrect position: " + rotatedPos);
}
}
/**
* Configures the rounded corner drawable's view matrix based on the gravity.
*
* The gravity describes which corner to configure for, and the drawable we are rotating is
* assumed to be oriented for the top-left corner of the device regardless of the target corner.
* Therefore we need to rotate 180 degrees to get a bottom-left corner, and mirror in the x- or
* y-axis for the top-right and bottom-left corners.
*/
private void setRoundedCornerOrientation(View corner, int gravity) {
corner.setRotation(0);
corner.setScaleX(1);
corner.setScaleY(1);
switch (gravity) {
case Gravity.TOP | Gravity.LEFT:
return;
case Gravity.TOP | Gravity.RIGHT:
corner.setScaleX(-1); // flip X axis
return;
case Gravity.BOTTOM | Gravity.LEFT:
corner.setScaleY(-1); // flip Y axis
return;
case Gravity.BOTTOM | Gravity.RIGHT:
corner.setRotation(180);
return;
default:
throw new IllegalArgumentException("Unsupported gravity: " + gravity);
}
}
private boolean hasRoundedCorners() {
return mRoundedDefault.x > 0
|| mRoundedDefaultBottom.x > 0
|| mRoundedDefaultTop.x > 0
|| mIsRoundedCornerMultipleRadius;
}
private boolean shouldShowRoundedCorner(@BoundsPosition int pos) {
if (!hasRoundedCorners()) {
return false;
}
DisplayCutout cutout = getCutout();
// for cutout is null or cutout with only waterfall.
final boolean emptyBoundsOrWaterfall = cutout == null || cutout.isBoundsEmpty();
// Shows rounded corner on left and right overlays only when there is no top or bottom
// cutout.
final int rotatedTop = getBoundPositionFromRotation(BOUNDS_POSITION_TOP, mRotation);
final int rotatedBottom = getBoundPositionFromRotation(BOUNDS_POSITION_BOTTOM, mRotation);
if (emptyBoundsOrWaterfall || !cutout.getBoundingRectsAll()[rotatedTop].isEmpty()
|| !cutout.getBoundingRectsAll()[rotatedBottom].isEmpty()) {
return pos == BOUNDS_POSITION_TOP || pos == BOUNDS_POSITION_BOTTOM;
} else {
return pos == BOUNDS_POSITION_LEFT || pos == BOUNDS_POSITION_RIGHT;
}
}
private boolean shouldDrawCutout() {
return shouldDrawCutout(mContext);
}
static boolean shouldDrawCutout(Context context) {
return context.getResources().getBoolean(
com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
}
private void updateLayoutParams() {
if (mOverlays == null) {
return;
}
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] == null) {
continue;
}
mWindowManager.updateViewLayout(mOverlays[i], getWindowLayoutParams(i));
}
}
@Override
public void onTuningChanged(String key, String newValue) {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
Log.i(TAG, "ScreenDecorations is disabled");
return;
}
mExecutor.execute(() -> {
if (mOverlays == null) return;
if (SIZE.equals(key)) {
Point size = mRoundedDefault;
Point sizeTop = mRoundedDefaultTop;
Point sizeBottom = mRoundedDefaultBottom;
if (newValue != null) {
try {
int s = (int) (Integer.parseInt(newValue) * mDensity);
size = new Point(s, s);
} catch (Exception e) {
}
}
updateRoundedCornerSize(size, sizeTop, sizeBottom);
}
});
}
private void updateRoundedCornerSize(
Point sizeDefault,
Point sizeTop,
Point sizeBottom) {
if (mOverlays == null) {
return;
}
if (sizeTop.x == 0) {
sizeTop = sizeDefault;
}
if (sizeBottom.x == 0) {
sizeBottom = sizeDefault;
}
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] == null) {
continue;
}
if (i == BOUNDS_POSITION_LEFT || i == BOUNDS_POSITION_RIGHT) {
if (mRotation == ROTATION_270) {
setSize(mOverlays[i].findViewById(R.id.left), sizeBottom);
setSize(mOverlays[i].findViewById(R.id.right), sizeTop);
} else {
setSize(mOverlays[i].findViewById(R.id.left), sizeTop);
setSize(mOverlays[i].findViewById(R.id.right), sizeBottom);
}
} else if (i == BOUNDS_POSITION_TOP) {
setSize(mOverlays[i].findViewById(R.id.left), sizeTop);
setSize(mOverlays[i].findViewById(R.id.right), sizeTop);
} else if (i == BOUNDS_POSITION_BOTTOM) {
setSize(mOverlays[i].findViewById(R.id.left), sizeBottom);
setSize(mOverlays[i].findViewById(R.id.right), sizeBottom);
}
}
}
@VisibleForTesting
protected void setSize(View view, Point pixelSize) {
LayoutParams params = view.getLayoutParams();
params.width = pixelSize.x;
params.height = pixelSize.y;
view.setLayoutParams(params);
}
public static class DisplayCutoutView extends View implements DisplayManager.DisplayListener,
RegionInterceptableView {
private static final float HIDDEN_CAMERA_PROTECTION_SCALE = 0.5f;
private Display.Mode mDisplayMode = null;
private final DisplayInfo mInfo = new DisplayInfo();
private final Paint mPaint = new Paint();
private final List<Rect> mBounds = new ArrayList();
private final Rect mBoundingRect = new Rect();
private final Path mBoundingPath = new Path();
// Don't initialize these yet because they may never exist
private RectF mProtectionRect;
private RectF mProtectionRectOrig;
private Path mProtectionPath;
private Path mProtectionPathOrig;
private Rect mTotalBounds = new Rect();
// Whether or not to show the cutout protection path
private boolean mShowProtection = false;
private final int[] mLocation = new int[2];
private final ScreenDecorations mDecorations;
private int mColor = Color.BLACK;
private int mRotation;
private int mInitialPosition;
private int mPosition;
private float mCameraProtectionProgress = HIDDEN_CAMERA_PROTECTION_SCALE;
private ValueAnimator mCameraProtectionAnimator;
public DisplayCutoutView(Context context, @BoundsPosition int pos,
ScreenDecorations decorations) {
super(context);
mInitialPosition = pos;
mDecorations = decorations;
setId(R.id.display_cutout);
if (DEBUG) {
getViewTreeObserver().addOnDrawListener(() -> Log.i(TAG,
getWindowTitleByPos(pos) + " drawn in rot " + mRotation));
}
}
public void setColor(int color) {
mColor = color;
invalidate();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
getHandler());
update();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
getLocationOnScreen(mLocation);
canvas.translate(-mLocation[0], -mLocation[1]);
if (!mBoundingPath.isEmpty()) {
mPaint.setColor(mColor);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
canvas.drawPath(mBoundingPath, mPaint);
}
if (mCameraProtectionProgress > HIDDEN_CAMERA_PROTECTION_SCALE
&& !mProtectionRect.isEmpty()) {
canvas.scale(mCameraProtectionProgress, mCameraProtectionProgress,
mProtectionRect.centerX(), mProtectionRect.centerY());
canvas.drawPath(mProtectionPath, mPaint);
}
}
@Override
public void onDisplayAdded(int displayId) {
}
@Override
public void onDisplayRemoved(int displayId) {
}
@Override
public void onDisplayChanged(int displayId) {
Display.Mode oldMode = mDisplayMode;
mDisplayMode = getDisplay().getMode();
// Display mode hasn't meaningfully changed, we can ignore it
if (!modeChanged(oldMode, mDisplayMode)) {
return;
}
if (displayId == getDisplay().getDisplayId()) {
update();
}
}
private boolean modeChanged(Display.Mode oldMode, Display.Mode newMode) {
if (oldMode == null) {
return true;
}
boolean changed = false;
changed |= oldMode.getPhysicalHeight() != newMode.getPhysicalHeight();
changed |= oldMode.getPhysicalWidth() != newMode.getPhysicalWidth();
// We purposely ignore refresh rate and id changes here, because we don't need to
// invalidate for those, and they can trigger the refresh rate to increase
return changed;
}
public void setRotation(int rotation) {
mRotation = rotation;
update();
}
void setProtection(Path protectionPath, Rect pathBounds) {
if (mProtectionPathOrig == null) {
mProtectionPathOrig = new Path();
mProtectionPath = new Path();
}
mProtectionPathOrig.set(protectionPath);
if (mProtectionRectOrig == null) {
mProtectionRectOrig = new RectF();
mProtectionRect = new RectF();
}
mProtectionRectOrig.set(pathBounds);
}
void setShowProtection(boolean shouldShow) {
if (mShowProtection == shouldShow) {
return;
}
mShowProtection = shouldShow;
updateBoundingPath();
// Delay the relayout until the end of the animation when hiding the cutout,
// otherwise we'd clip it.
if (mShowProtection) {
requestLayout();
}
if (mCameraProtectionAnimator != null) {
mCameraProtectionAnimator.cancel();
}
mCameraProtectionAnimator = ValueAnimator.ofFloat(mCameraProtectionProgress,
mShowProtection ? 1.0f : HIDDEN_CAMERA_PROTECTION_SCALE).setDuration(750);
mCameraProtectionAnimator.setInterpolator(Interpolators.DECELERATE_QUINT);
mCameraProtectionAnimator.addUpdateListener(animation -> {
mCameraProtectionProgress = (float) animation.getAnimatedValue();
invalidate();
});
mCameraProtectionAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
mCameraProtectionAnimator = null;
if (!mShowProtection) {
requestLayout();
}
}
});
mCameraProtectionAnimator.start();
}
private void update() {
if (!isAttachedToWindow() || mDecorations.mPendingRotationChange) {
return;
}
mPosition = getBoundPositionFromRotation(mInitialPosition, mRotation);
requestLayout();
getDisplay().getDisplayInfo(mInfo);
mBounds.clear();
mBoundingRect.setEmpty();
mBoundingPath.reset();
int newVisible;
if (shouldDrawCutout(getContext()) && hasCutout()) {
mBounds.addAll(mInfo.displayCutout.getBoundingRects());
localBounds(mBoundingRect);
updateGravity();
updateBoundingPath();
invalidate();
newVisible = VISIBLE;
} else {
newVisible = GONE;
}
if (newVisible != getVisibility()) {
setVisibility(newVisible);
}
}
private void updateBoundingPath() {
int lw = mInfo.logicalWidth;
int lh = mInfo.logicalHeight;
boolean flipped = mInfo.rotation == ROTATION_90 || mInfo.rotation == ROTATION_270;
int dw = flipped ? lh : lw;
int dh = flipped ? lw : lh;
Path path = DisplayCutout.pathFromResources(getResources(), dw, dh);
if (path != null) {
mBoundingPath.set(path);
} else {
mBoundingPath.reset();
}
Matrix m = new Matrix();
transformPhysicalToLogicalCoordinates(mInfo.rotation, dw, dh, m);
mBoundingPath.transform(m);
if (mProtectionPathOrig != null) {
// Reset the protection path so we don't aggregate rotations
mProtectionPath.set(mProtectionPathOrig);
mProtectionPath.transform(m);
m.mapRect(mProtectionRect, mProtectionRectOrig);
}
}
private static void transformPhysicalToLogicalCoordinates(@Surface.Rotation int rotation,
@Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) {
switch (rotation) {
case ROTATION_0:
out.reset();
break;
case ROTATION_90:
out.setRotate(270);
out.postTranslate(0, physicalWidth);
break;
case ROTATION_180:
out.setRotate(180);
out.postTranslate(physicalWidth, physicalHeight);
break;
case ROTATION_270:
out.setRotate(90);
out.postTranslate(physicalHeight, 0);
break;
default:
throw new IllegalArgumentException("Unknown rotation: " + rotation);
}
}
private void updateGravity() {
LayoutParams lp = getLayoutParams();
if (lp instanceof FrameLayout.LayoutParams) {
FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) lp;
int newGravity = getGravity(mInfo.displayCutout);
if (flp.gravity != newGravity) {
flp.gravity = newGravity;
setLayoutParams(flp);
}
}
}
private boolean hasCutout() {
final DisplayCutout displayCutout = mInfo.displayCutout;
if (displayCutout == null) {
return false;
}
if (mPosition == BOUNDS_POSITION_LEFT) {
return !displayCutout.getBoundingRectLeft().isEmpty();
} else if (mPosition == BOUNDS_POSITION_TOP) {
return !displayCutout.getBoundingRectTop().isEmpty();
} else if (mPosition == BOUNDS_POSITION_BOTTOM) {
return !displayCutout.getBoundingRectBottom().isEmpty();
} else if (mPosition == BOUNDS_POSITION_RIGHT) {
return !displayCutout.getBoundingRectRight().isEmpty();
}
return false;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mBounds.isEmpty()) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
return;
}
if (mShowProtection) {
// Make sure that our measured height encompases the protection
mTotalBounds.union(mBoundingRect);
mTotalBounds.union((int) mProtectionRect.left, (int) mProtectionRect.top,
(int) mProtectionRect.right, (int) mProtectionRect.bottom);
setMeasuredDimension(
resolveSizeAndState(mTotalBounds.width(), widthMeasureSpec, 0),
resolveSizeAndState(mTotalBounds.height(), heightMeasureSpec, 0));
} else {
setMeasuredDimension(
resolveSizeAndState(mBoundingRect.width(), widthMeasureSpec, 0),
resolveSizeAndState(mBoundingRect.height(), heightMeasureSpec, 0));
}
}
public static void boundsFromDirection(DisplayCutout displayCutout, int gravity,
Rect out) {
switch (gravity) {
case Gravity.TOP:
out.set(displayCutout.getBoundingRectTop());
break;
case Gravity.LEFT:
out.set(displayCutout.getBoundingRectLeft());
break;
case Gravity.BOTTOM:
out.set(displayCutout.getBoundingRectBottom());
break;
case Gravity.RIGHT:
out.set(displayCutout.getBoundingRectRight());
break;
default:
out.setEmpty();
}
}
private void localBounds(Rect out) {
DisplayCutout displayCutout = mInfo.displayCutout;
boundsFromDirection(displayCutout, getGravity(displayCutout), out);
}
private int getGravity(DisplayCutout displayCutout) {
if (mPosition == BOUNDS_POSITION_LEFT) {
if (!displayCutout.getBoundingRectLeft().isEmpty()) {
return Gravity.LEFT;
}
} else if (mPosition == BOUNDS_POSITION_TOP) {
if (!displayCutout.getBoundingRectTop().isEmpty()) {
return Gravity.TOP;
}
} else if (mPosition == BOUNDS_POSITION_BOTTOM) {
if (!displayCutout.getBoundingRectBottom().isEmpty()) {
return Gravity.BOTTOM;
}
} else if (mPosition == BOUNDS_POSITION_RIGHT) {
if (!displayCutout.getBoundingRectRight().isEmpty()) {
return Gravity.RIGHT;
}
}
return Gravity.NO_GRAVITY;
}
@Override
public boolean shouldInterceptTouch() {
return mInfo.displayCutout != null && getVisibility() == VISIBLE;
}
@Override
public Region getInterceptRegion() {
if (mInfo.displayCutout == null) {
return null;
}
View rootView = getRootView();
Region cutoutBounds = rectsToRegion(
mInfo.displayCutout.getBoundingRects());
// Transform to window's coordinate space
rootView.getLocationOnScreen(mLocation);
cutoutBounds.translate(-mLocation[0], -mLocation[1]);
// Intersect with window's frame
cutoutBounds.op(rootView.getLeft(), rootView.getTop(), rootView.getRight(),
rootView.getBottom(), Region.Op.INTERSECT);
return cutoutBounds;
}
}
/**
* A pre-draw listener, that cancels the draw and restarts the traversal with the updated
* window attributes.
*/
private class RestartingPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
private final View mView;
private final int mTargetRotation;
private final int mPosition;
private RestartingPreDrawListener(View view, @BoundsPosition int position,
int targetRotation) {
mView = view;
mTargetRotation = targetRotation;
mPosition = position;
}
@Override
public boolean onPreDraw() {
mView.getViewTreeObserver().removeOnPreDrawListener(this);
if (mTargetRotation == mRotation) {
if (DEBUG) {
Log.i(TAG, getWindowTitleByPos(mPosition) + " already in target rot "
+ mTargetRotation + ", allow draw without restarting it");
}
return true;
}
mPendingRotationChange = false;
// This changes the window attributes - we need to restart the traversal for them to
// take effect.
updateOrientation();
if (DEBUG) {
Log.i(TAG, getWindowTitleByPos(mPosition)
+ " restarting listener fired, restarting draw for rot " + mRotation);
}
mView.invalidate();
return false;
}
}
/**
* A pre-draw listener, that validates that the rotation we draw in matches the displays
* rotation before continuing the draw.
*
* This is to prevent a race condition, where we have not received the display changed event
* yet, and would thus draw in an old orientation.
*/
private class ValidatingPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
private final View mView;
public ValidatingPreDrawListener(View view) {
mView = view;
}
@Override
public boolean onPreDraw() {
final int displayRotation = mContext.getDisplay().getRotation();
if (displayRotation != mRotation && !mPendingRotationChange) {
if (DEBUG) {
Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot "
+ displayRotation + ". Restarting draw");
}
mView.invalidate();
return false;
}
return true;
}
}
}