blob: e09c16cafe768b4d35a97d387f72aec8ec5fd838 [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.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.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.annotation.IdRes;
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.pm.ActivityInfo;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.hardware.display.DisplayManager;
import android.hardware.graphics.common.AlphaInterpretation;
import android.hardware.graphics.common.DisplayDecorationSupport;
import android.os.Handler;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.provider.Settings.Secure;
import android.util.DisplayUtils;
import android.util.Log;
import android.util.Size;
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.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 androidx.annotation.VisibleForTesting;
import com.android.internal.util.Preconditions;
import com.android.settingslib.Utils;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.decor.DecorProvider;
import com.android.systemui.decor.DecorProviderFactory;
import com.android.systemui.decor.DecorProviderKt;
import com.android.systemui.decor.FaceScanningProviderFactory;
import com.android.systemui.decor.OverlayWindow;
import com.android.systemui.decor.PrivacyDotDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerDecorProviderFactory;
import com.android.systemui.decor.RoundedCornerResDelegate;
import com.android.systemui.qs.SettingObserver;
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.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Executor;
import javax.inject.Inject;
import kotlin.Pair;
/**
* 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 CoreStartable implements Tunable , Dumpable {
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;
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;
@VisibleForTesting
DisplayManager.DisplayListener mDisplayListener;
private CameraAvailabilityListener mCameraListener;
private final UserTracker mUserTracker;
private final PrivacyDotViewController mDotViewController;
private final ThreadFactory mThreadFactory;
private final DecorProviderFactory mDotFactory;
private final FaceScanningProviderFactory mFaceScanningFactory;
public final int mFaceScanningViewId;
@VisibleForTesting
protected RoundedCornerResDelegate mRoundedCornerResDelegate;
@VisibleForTesting
protected DecorProviderFactory mRoundedCornerFactory;
private int mProviderRefreshToken = 0;
@VisibleForTesting
protected OverlayWindow[] mOverlays = null;
@VisibleForTesting
@Nullable
DisplayCutoutView[] mCutoutViews;
@VisibleForTesting
ViewGroup mScreenDecorHwcWindow;
@VisibleForTesting
ScreenDecorHwcLayer mScreenDecorHwcLayer;
private WindowManager mWindowManager;
private int mRotation;
private SettingObserver mColorInversionSetting;
private DelayableExecutor mExecutor;
private Handler mHandler;
boolean mPendingConfigChange;
@VisibleForTesting
String mDisplayUniqueId;
private int mTintColor = Color.BLACK;
@VisibleForTesting
protected DisplayDecorationSupport mHwcScreenDecorationSupport;
private Display.Mode mDisplayMode;
@VisibleForTesting
protected DisplayInfo mDisplayInfo = new DisplayInfo();
@VisibleForTesting
protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
if (mFaceScanningFactory.shouldShowFaceScanningAnim()) {
DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView(
mFaceScanningViewId);
if (overlay != null) {
overlay.setProtection(protectionPath, bounds);
overlay.enableShowProtection(true);
updateOverlayWindowVisibilityIfViewExists(
overlay.findViewById(mFaceScanningViewId));
// immediately return, bc FaceScanningOverlay also renders the camera
// protection, so we don't need to show the camera protection in
// mScreenDecorHwcLayer or mCutoutViews
return;
}
}
if (mScreenDecorHwcLayer != null) {
mScreenDecorHwcLayer.setProtection(protectionPath, bounds);
mScreenDecorHwcLayer.enableShowProtection(true);
return;
}
if (mCutoutViews == null) {
Log.w(TAG, "DisplayCutoutView not initialized onApplyCameraProtection");
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.enableShowProtection(true);
}
}
}
@VisibleForTesting
protected void hideCameraProtection() {
FaceScanningOverlay faceScanningOverlay =
(FaceScanningOverlay) getOverlayView(mFaceScanningViewId);
if (faceScanningOverlay != null) {
faceScanningOverlay.setHideOverlayRunnable(() -> {
updateOverlayWindowVisibilityIfViewExists(
faceScanningOverlay.findViewById(mFaceScanningViewId));
});
faceScanningOverlay.enableShowProtection(false);
}
if (mScreenDecorHwcLayer != null) {
mScreenDecorHwcLayer.enableShowProtection(false);
return;
}
if (mCutoutViews == null) {
Log.w(TAG, "DisplayCutoutView not initialized onHideCameraProtection");
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.enableShowProtection(false);
}
}
}
private CameraAvailabilityListener.CameraTransitionCallback mCameraTransitionCallback =
new CameraAvailabilityListener.CameraTransitionCallback() {
@Override
public void onApplyCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) {
showCameraProtection(protectionPath, bounds);
}
@Override
public void onHideCameraProtection() {
hideCameraProtection();
}
};
@VisibleForTesting
PrivacyDotViewController.ShowingListener mPrivacyDotShowingListener =
new PrivacyDotViewController.ShowingListener() {
@Override
public void onPrivacyDotShown(@Nullable View v) {
updateOverlayWindowVisibilityIfViewExists(v);
}
@Override
public void onPrivacyDotHidden(@Nullable View v) {
updateOverlayWindowVisibilityIfViewExists(v);
}
};
@VisibleForTesting
protected void updateOverlayWindowVisibilityIfViewExists(@Nullable View view) {
if (view == null) {
return;
}
mExecutor.execute(() -> {
// We don't need to control the window visibility if rounded corners or cutout is drawn
// on sw layer since the overlay windows are always visible in this case.
if (mOverlays == null || !shouldOptimizeVisibility()) {
return;
}
for (final OverlayWindow overlay : mOverlays) {
if (overlay == null) {
continue;
}
if (overlay.getView(view.getId()) != null) {
overlay.getRootView().setVisibility(getWindowVisibility(overlay, true));
return;
}
}
});
}
private static boolean eq(DisplayDecorationSupport a, DisplayDecorationSupport b) {
if (a == null) return (b == null);
if (b == null) return false;
return a.format == b.format && a.alphaInterpretation == b.alphaInterpretation;
}
@Inject
public ScreenDecorations(Context context,
@Main Executor mainExecutor,
SecureSettings secureSettings,
BroadcastDispatcher broadcastDispatcher,
TunerService tunerService,
UserTracker userTracker,
PrivacyDotViewController dotViewController,
ThreadFactory threadFactory,
PrivacyDotDecorProviderFactory dotFactory,
FaceScanningProviderFactory faceScanningFactory) {
super(context);
mMainExecutor = mainExecutor;
mSecureSettings = secureSettings;
mBroadcastDispatcher = broadcastDispatcher;
mTunerService = tunerService;
mUserTracker = userTracker;
mDotViewController = dotViewController;
mThreadFactory = threadFactory;
mDotFactory = dotFactory;
mFaceScanningFactory = faceScanningFactory;
mFaceScanningViewId = com.android.systemui.R.id.face_scanning_anim;
}
@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 boolean isPrivacyDotEnabled() {
return mDotFactory.getHasProviders();
}
@NonNull
private List<DecorProvider> getProviders(boolean hasHwLayer) {
List<DecorProvider> decorProviders = new ArrayList<>(mDotFactory.getProviders());
decorProviders.addAll(mFaceScanningFactory.getProviders());
if (!hasHwLayer) {
decorProviders.addAll(mRoundedCornerFactory.getProviders());
}
return decorProviders;
}
/**
* Check that newProviders is the same list with decorProviders inside mOverlay.
* @param newProviders expected comparing DecorProviders
* @return true if same provider list
*/
@VisibleForTesting
boolean hasSameProviders(@NonNull List<DecorProvider> newProviders) {
final ArrayList<Integer> overlayViewIds = new ArrayList<>();
if (mOverlays != null) {
for (OverlayWindow overlay : mOverlays) {
if (overlay == null) {
continue;
}
overlayViewIds.addAll(overlay.getViewIds());
}
}
if (overlayViewIds.size() != newProviders.size()) {
return false;
}
for (DecorProvider provider: newProviders) {
if (!overlayViewIds.contains(provider.getViewId())) {
return false;
}
}
return true;
}
private void startOnScreenDecorationsThread() {
mWindowManager = mContext.getSystemService(WindowManager.class);
mDisplayManager = mContext.getSystemService(DisplayManager.class);
mContext.getDisplay().getDisplayInfo(mDisplayInfo);
mRotation = mDisplayInfo.rotation;
mDisplayMode = mDisplayInfo.getMode();
mDisplayUniqueId = mDisplayInfo.uniqueId;
mRoundedCornerResDelegate = new RoundedCornerResDelegate(mContext.getResources(),
mDisplayUniqueId);
mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(
getPhysicalPixelDisplaySizeRatio());
mRoundedCornerFactory = new RoundedCornerDecorProviderFactory(mRoundedCornerResDelegate);
mHwcScreenDecorationSupport = mContext.getDisplay().getDisplayDecorationSupport();
updateHwLayerRoundedCornerDrawable();
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) {
mContext.getDisplay().getDisplayInfo(mDisplayInfo);
final int newRotation = mDisplayInfo.rotation;
final Display.Mode newDisplayMode = mDisplayInfo.getMode();
if ((mOverlays != null || mScreenDecorHwcWindow != null)
&& (mRotation != newRotation
|| displayModeChanged(mDisplayMode, newDisplayMode))) {
// 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.
mPendingConfigChange = true;
if (DEBUG) {
if (mRotation != newRotation) {
Log.i(TAG, "Rotation changed, deferring " + newRotation
+ ", staying at " + mRotation);
}
if (displayModeChanged(mDisplayMode, newDisplayMode)) {
Log.i(TAG, "Resolution changed, deferring " + newDisplayMode
+ ", staying at " + mDisplayMode);
}
}
if (mOverlays != null) {
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] != null) {
final ViewGroup overlayView = mOverlays[i].getRootView();
overlayView.getViewTreeObserver().addOnPreDrawListener(
new RestartingPreDrawListener(
overlayView, i, newRotation, newDisplayMode));
}
}
}
if (mScreenDecorHwcWindow != null) {
mScreenDecorHwcWindow.getViewTreeObserver().addOnPreDrawListener(
new RestartingPreDrawListener(
mScreenDecorHwcWindow,
-1, // Pass -1 for views with no specific position.
newRotation, newDisplayMode));
}
if (mScreenDecorHwcLayer != null) {
mScreenDecorHwcLayer.pendingConfigChange = true;
}
}
final String newUniqueId = mDisplayInfo.uniqueId;
if (!Objects.equals(newUniqueId, mDisplayUniqueId)) {
mDisplayUniqueId = newUniqueId;
final DisplayDecorationSupport newScreenDecorationSupport =
mContext.getDisplay().getDisplayDecorationSupport();
mRoundedCornerResDelegate.updateDisplayUniqueId(newUniqueId, null);
// When providers or the value of mSupportHwcScreenDecoration is changed,
// re-setup the whole screen decoration.
if (!hasSameProviders(getProviders(newScreenDecorationSupport != null))
|| !eq(newScreenDecorationSupport, mHwcScreenDecorationSupport)) {
mHwcScreenDecorationSupport = newScreenDecorationSupport;
removeAllOverlays();
setupDecorations();
return;
}
if (mScreenDecorHwcLayer != null) {
updateHwLayerRoundedCornerDrawable();
updateHwLayerRoundedCornerExistAndSize();
}
updateOverlayProviderViews();
}
final float newRatio = getPhysicalPixelDisplaySizeRatio();
if (mRoundedCornerResDelegate.getPhysicalPixelDisplaySizeRatio() != newRatio) {
mRoundedCornerResDelegate.setPhysicalPixelDisplaySizeRatio(newRatio);
if (mScreenDecorHwcLayer != null) {
updateHwLayerRoundedCornerExistAndSize();
}
updateOverlayProviderViews();
}
if (mCutoutViews != null) {
final int size = mCutoutViews.length;
for (int i = 0; i < size; i++) {
final DisplayCutoutView cutoutView = mCutoutViews[i];
if (cutoutView == null) {
continue;
}
cutoutView.onDisplayChanged(displayId);
}
}
if (mScreenDecorHwcLayer != null) {
mScreenDecorHwcLayer.onDisplayChanged(displayId);
}
}
};
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler);
updateConfiguration();
}
@Nullable
private View getOverlayView(@IdRes int id) {
if (mOverlays == null) {
return null;
}
for (final OverlayWindow overlay : mOverlays) {
if (overlay == null) {
continue;
}
final View view = overlay.getView(id);
if (view != null) {
return view;
}
}
return null;
}
private void removeRedundantOverlayViews(@NonNull List<DecorProvider> decorProviders) {
if (mOverlays == null) {
return;
}
int[] viewIds = decorProviders.stream().mapToInt(DecorProvider::getViewId).toArray();
for (final OverlayWindow overlay : mOverlays) {
if (overlay == null) {
continue;
}
overlay.removeRedundantViews(viewIds);
}
}
private void removeOverlayView(@IdRes int id) {
if (mOverlays == null) {
return;
}
for (final OverlayWindow overlay : mOverlays) {
if (overlay == null) {
continue;
}
overlay.removeView(id);
}
}
private void setupDecorations() {
if (hasRoundedCorners() || shouldDrawCutout() || isPrivacyDotEnabled()
|| mFaceScanningFactory.getHasProviders()) {
List<DecorProvider> decorProviders = getProviders(mHwcScreenDecorationSupport != null);
removeRedundantOverlayViews(decorProviders);
if (mHwcScreenDecorationSupport != null) {
createHwcOverlay();
} else {
removeHwcOverlay();
}
final DisplayCutout cutout = getCutout();
final boolean shouldOptimizeVisibility = shouldOptimizeVisibility();
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (shouldShowSwLayerCutout(i, cutout)
|| shouldShowSwLayerFaceScan(i, cutout)
|| shouldShowSwLayerRoundedCorner(i, cutout)
|| shouldShowSwLayerPrivacyDot(i, cutout)) {
Pair<List<DecorProvider>, List<DecorProvider>> pair =
DecorProviderKt.partitionAlignedBound(decorProviders, i);
decorProviders = pair.getSecond();
createOverlay(i, pair.getFirst(), shouldOptimizeVisibility);
} else {
removeOverlay(i);
}
}
if (shouldOptimizeVisibility) {
mDotViewController.setShowingListener(mPrivacyDotShowingListener);
} else {
mDotViewController.setShowingListener(null);
}
final View tl, tr, bl, br;
if ((tl = getOverlayView(R.id.privacy_dot_top_left_container)) != null
&& (tr = getOverlayView(R.id.privacy_dot_top_right_container)) != null
&& (bl = getOverlayView(R.id.privacy_dot_bottom_left_container)) != null
&& (br = getOverlayView(R.id.privacy_dot_bottom_right_container)) != null) {
// Overlays have been created, send the dots to the controller
//TODO: need a better way to do this
mDotViewController.initialize(tl, tr, bl, br);
}
} else {
removeAllOverlays();
removeHwcOverlay();
}
if (hasOverlays() || hasHwcOverlay()) {
if (mIsRegistered) {
return;
}
mMainExecutor.execute(() -> mTunerService.addTunable(this, SIZE));
// Watch color inversion and invert the overlay as needed.
if (mColorInversionSetting == null) {
mColorInversionSetting = new SettingObserver(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);
updateColorInversion(mColorInversionSetting.getValue());
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].getRootView());
mOverlays[pos] = null;
}
@View.Visibility
private int getWindowVisibility(@NonNull OverlayWindow overlay,
boolean shouldOptimizeVisibility) {
if (!shouldOptimizeVisibility) {
// All overlays have visible views so there's no need to optimize visibility.
// For example, the rounded corners could exist in each overlay and since the rounded
// corners are always visible, there's no need to optimize visibility.
return View.VISIBLE;
}
// Optimize if it's just the privacy dot & face scanning animation, since the privacy
// dot and face scanning overlay aren't always visible.
int[] ids = {
R.id.privacy_dot_top_left_container,
R.id.privacy_dot_top_right_container,
R.id.privacy_dot_bottom_left_container,
R.id.privacy_dot_bottom_right_container,
mFaceScanningViewId
};
for (int id: ids) {
final View notAlwaysVisibleViews = overlay.getView(id);
if (notAlwaysVisibleViews != null
&& notAlwaysVisibleViews.getVisibility() == View.VISIBLE) {
// Overlay is VISIBLE if one the views inside this overlay is VISIBLE
return View.VISIBLE;
}
}
// Only non-visible views in this overlay, so set overlay to INVISIBLE
return View.INVISIBLE;
}
private void createOverlay(
@BoundsPosition int pos,
@NonNull List<DecorProvider> decorProviders,
boolean shouldOptimizeVisibility) {
if (mOverlays == null) {
mOverlays = new OverlayWindow[BOUNDS_POSITION_LENGTH];
}
if (mOverlays[pos] != null) {
initOverlay(mOverlays[pos], decorProviders, shouldOptimizeVisibility);
return;
}
mOverlays[pos] = new OverlayWindow(mContext);
initOverlay(mOverlays[pos], decorProviders, shouldOptimizeVisibility);
final ViewGroup overlayView = mOverlays[pos].getRootView();
overlayView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
overlayView.setAlpha(0);
overlayView.setForceDarkAllowed(false);
// Only show cutout in mOverlays when hwc doesn't support screen decoration
if (mHwcScreenDecorationSupport == null) {
if (mCutoutViews == null) {
mCutoutViews = new DisplayCutoutView[BOUNDS_POSITION_LENGTH];
}
mCutoutViews[pos] = new DisplayCutoutView(mContext, pos);
overlayView.addView(mCutoutViews[pos]);
mCutoutViews[pos].updateRotation(mRotation);
}
mWindowManager.addView(overlayView, getWindowLayoutParams(pos));
overlayView.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) {
overlayView.removeOnLayoutChangeListener(this);
overlayView.animate()
.alpha(1)
.setDuration(1000)
.start();
}
});
overlayView.getRootView().getViewTreeObserver().addOnPreDrawListener(
new ValidatingPreDrawListener(overlayView.getRootView()));
}
private boolean hasHwcOverlay() {
return mScreenDecorHwcWindow != null;
}
private void removeHwcOverlay() {
if (mScreenDecorHwcWindow == null) {
return;
}
mWindowManager.removeViewImmediate(mScreenDecorHwcWindow);
mScreenDecorHwcWindow = null;
mScreenDecorHwcLayer = null;
}
private void createHwcOverlay() {
if (mScreenDecorHwcWindow != null) {
return;
}
mScreenDecorHwcWindow = (ViewGroup) LayoutInflater.from(mContext).inflate(
R.layout.screen_decor_hwc_layer, null);
mScreenDecorHwcLayer = new ScreenDecorHwcLayer(mContext, mHwcScreenDecorationSupport);
mScreenDecorHwcWindow.addView(mScreenDecorHwcLayer, new FrameLayout.LayoutParams(
MATCH_PARENT, MATCH_PARENT, Gravity.TOP | Gravity.START));
mWindowManager.addView(mScreenDecorHwcWindow, getHwcWindowLayoutParams());
updateHwLayerRoundedCornerExistAndSize();
updateHwLayerRoundedCornerDrawable();
mScreenDecorHwcWindow.getViewTreeObserver().addOnPreDrawListener(
new ValidatingPreDrawListener(mScreenDecorHwcWindow));
}
/**
* Init OverlayWindow with decorProviders
*/
private void initOverlay(
@NonNull OverlayWindow overlay,
@NonNull List<DecorProvider> decorProviders,
boolean shouldOptimizeVisibility) {
if (!overlay.hasSameProviders(decorProviders)) {
decorProviders.forEach(provider -> {
if (overlay.getView(provider.getViewId()) != null) {
return;
}
removeOverlayView(provider.getViewId());
overlay.addDecorProvider(provider, mRotation);
});
}
// Use visibility of privacy dot views & face scanning view to determine the overlay's
// visibility if the screen decoration SW layer overlay isn't persistenly showing
// (ie: rounded corners always showing in SW layer)
overlay.getRootView().setVisibility(getWindowVisibility(overlay, shouldOptimizeVisibility));
}
@VisibleForTesting
WindowManager.LayoutParams getWindowLayoutParams(@BoundsPosition int pos) {
final WindowManager.LayoutParams lp = getWindowLayoutBaseParams();
lp.width = getWidthLayoutParamByPos(pos);
lp.height = getHeightLayoutParamByPos(pos);
lp.setTitle(getWindowTitleByPos(pos));
lp.gravity = getOverlayWindowGravity(pos);
return lp;
}
private WindowManager.LayoutParams getHwcWindowLayoutParams() {
final WindowManager.LayoutParams lp = getWindowLayoutBaseParams();
lp.width = MATCH_PARENT;
lp.height = MATCH_PARENT;
lp.setTitle("ScreenDecorHwcOverlay");
lp.gravity = Gravity.TOP | Gravity.START;
if (!DEBUG_COLOR) {
lp.setColorMode(ActivityInfo.COLOR_MODE_A8);
}
return lp;
}
private WindowManager.LayoutParams getWindowLayoutBaseParams() {
final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
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.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 static boolean displayModeChanged(Display.Mode oldMode, Display.Mode newMode) {
if (oldMode == null) {
return true;
}
// 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 oldMode.getPhysicalWidth() != newMode.getPhysicalWidth()
|| oldMode.getPhysicalHeight() != newMode.getPhysicalHeight();
}
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);
}
}
@VisibleForTesting
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) {
mTintColor = colorsInvertedValue != 0 ? Color.WHITE : Color.BLACK;
if (DEBUG_COLOR) {
mTintColor = Color.RED;
}
if (mOverlays == null) {
return;
}
// When the hwc supports screen decorations, the layer will use the A8 color mode which
// won't be affected by the color inversion. If the composition goes the client composition
// route, the color inversion will be handled by the RenderEngine.
final Set<Integer> viewsMayNeedColorUpdate = new HashSet<>();
if (mHwcScreenDecorationSupport == null) {
ColorStateList tintList = ColorStateList.valueOf(mTintColor);
mRoundedCornerResDelegate.setColorTintList(tintList);
viewsMayNeedColorUpdate.add(R.id.rounded_corner_top_left);
viewsMayNeedColorUpdate.add(R.id.rounded_corner_top_right);
viewsMayNeedColorUpdate.add(R.id.rounded_corner_bottom_left);
viewsMayNeedColorUpdate.add(R.id.rounded_corner_bottom_right);
viewsMayNeedColorUpdate.add(R.id.display_cutout);
}
if (mFaceScanningFactory.getHasProviders()) {
viewsMayNeedColorUpdate.add(mFaceScanningViewId);
}
final Integer[] views = new Integer[viewsMayNeedColorUpdate.size()];
viewsMayNeedColorUpdate.toArray(views);
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] == null) {
continue;
}
final ViewGroup overlayView = mOverlays[i].getRootView();
final int size = overlayView.getChildCount();
View child;
for (int j = 0; j < size; j++) {
child = overlayView.getChildAt(j);
if (viewsMayNeedColorUpdate.contains(child.getId())
&& child instanceof DisplayCutoutView) {
((DisplayCutoutView) child).setColor(mTintColor);
}
}
mOverlays[i].onReloadResAndMeasure(views, mProviderRefreshToken,
mRotation, mDisplayUniqueId);
}
}
@VisibleForTesting
float getPhysicalPixelDisplaySizeRatio() {
mContext.getDisplay().getDisplayInfo(mDisplayInfo);
final Display.Mode maxDisplayMode =
DisplayUtils.getMaximumResolutionDisplayMode(mDisplayInfo.supportedModes);
if (maxDisplayMode == null) {
return 1f;
}
return DisplayUtils.getPhysicalPixelDisplaySizeRatio(
maxDisplayMode.getPhysicalWidth(), maxDisplayMode.getPhysicalHeight(),
mDisplayInfo.getNaturalWidth(), mDisplayInfo.getNaturalHeight());
}
@Override
protected void onConfigurationChanged(Configuration newConfig) {
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
Log.i(TAG, "ScreenDecorations is disabled");
return;
}
mExecutor.execute(() -> {
int oldRotation = mRotation;
mPendingConfigChange = false;
updateConfiguration();
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 static String alphaInterpretationToString(int alpha) {
switch (alpha) {
case AlphaInterpretation.COVERAGE: return "COVERAGE";
case AlphaInterpretation.MASK: return "MASK";
default: return "Unknown: " + alpha;
}
}
@Override
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("ScreenDecorations state:");
pw.println(" DEBUG_DISABLE_SCREEN_DECORATIONS:" + DEBUG_DISABLE_SCREEN_DECORATIONS);
if (DEBUG_DISABLE_SCREEN_DECORATIONS) {
return;
}
pw.println(" mIsPrivacyDotEnabled:" + isPrivacyDotEnabled());
pw.println(" shouldOptimizeOverlayVisibility:" + shouldOptimizeVisibility());
final boolean supportsShowingFaceScanningAnim = mFaceScanningFactory.getHasProviders();
pw.println(" supportsShowingFaceScanningAnim:" + supportsShowingFaceScanningAnim);
if (supportsShowingFaceScanningAnim) {
pw.println(" canShowFaceScanningAnim:"
+ mFaceScanningFactory.canShowFaceScanningAnim());
pw.println(" shouldShowFaceScanningAnim (at time dump was taken):"
+ mFaceScanningFactory.shouldShowFaceScanningAnim());
}
pw.println(" mPendingConfigChange:" + mPendingConfigChange);
if (mHwcScreenDecorationSupport != null) {
pw.println(" mHwcScreenDecorationSupport:");
pw.println(" format="
+ PixelFormat.formatToString(mHwcScreenDecorationSupport.format));
pw.println(" alphaInterpretation="
+ alphaInterpretationToString(mHwcScreenDecorationSupport.alphaInterpretation));
} else {
pw.println(" mHwcScreenDecorationSupport: null");
}
if (mScreenDecorHwcLayer != null) {
pw.println(" mScreenDecorHwcLayer:");
pw.println(" transparentRegion=" + mScreenDecorHwcLayer.transparentRect);
} else {
pw.println(" mScreenDecorHwcLayer: null");
}
if (mOverlays != null) {
pw.println(" mOverlays(left,top,right,bottom)=("
+ (mOverlays[BOUNDS_POSITION_LEFT] != null) + ","
+ (mOverlays[BOUNDS_POSITION_TOP] != null) + ","
+ (mOverlays[BOUNDS_POSITION_RIGHT] != null) + ","
+ (mOverlays[BOUNDS_POSITION_BOTTOM] != null) + ")");
for (int i = BOUNDS_POSITION_LEFT; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] != null) {
mOverlays[i].dump(pw, getWindowTitleByPos(i));
}
}
}
mRoundedCornerResDelegate.dump(pw, args);
}
private void updateConfiguration() {
Preconditions.checkState(mHandler.getLooper().getThread() == Thread.currentThread(),
"must call on " + mHandler.getLooper().getThread()
+ ", but was " + Thread.currentThread());
mContext.getDisplay().getDisplayInfo(mDisplayInfo);
final int newRotation = mDisplayInfo.rotation;
if (mRotation != newRotation) {
mDotViewController.setNewRotation(newRotation);
}
final Display.Mode newMod = mDisplayInfo.getMode();
if (!mPendingConfigChange
&& (newRotation != mRotation || displayModeChanged(mDisplayMode, newMod))) {
mRotation = newRotation;
mDisplayMode = newMod;
if (mScreenDecorHwcLayer != null) {
mScreenDecorHwcLayer.pendingConfigChange = false;
mScreenDecorHwcLayer.updateRotation(mRotation);
updateHwLayerRoundedCornerExistAndSize();
updateHwLayerRoundedCornerDrawable();
}
updateLayoutParams();
// update cutout view rotation
if (mCutoutViews != null) {
for (final DisplayCutoutView cutoutView: mCutoutViews) {
if (cutoutView == null) {
continue;
}
cutoutView.updateRotation(mRotation);
}
}
// update all provider views inside overlay
updateOverlayProviderViews();
}
if (mFaceScanningFactory.getHasProviders()) {
FaceScanningOverlay faceScanningOverlay =
(FaceScanningOverlay) getOverlayView(mFaceScanningViewId);
if (faceScanningOverlay != null) {
faceScanningOverlay.setFaceScanningAnimColor(
Utils.getColorAttrDefaultColor(faceScanningOverlay.getContext(),
com.android.systemui.R.attr.wallpaperTextColorAccent));
}
}
}
private boolean hasRoundedCorners() {
return mRoundedCornerFactory.getHasProviders();
}
private boolean isDefaultShownOverlayPos(@BoundsPosition int pos,
@Nullable DisplayCutout cutout) {
// 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 shouldShowSwLayerRoundedCorner(@BoundsPosition int pos,
@Nullable DisplayCutout cutout) {
return hasRoundedCorners() && isDefaultShownOverlayPos(pos, cutout)
&& mHwcScreenDecorationSupport == null;
}
private boolean shouldShowSwLayerPrivacyDot(@BoundsPosition int pos,
@Nullable DisplayCutout cutout) {
return isPrivacyDotEnabled() && isDefaultShownOverlayPos(pos, cutout);
}
private boolean shouldShowSwLayerFaceScan(@BoundsPosition int pos,
@Nullable DisplayCutout cutout) {
return mFaceScanningFactory.getHasProviders() && isDefaultShownOverlayPos(pos, cutout);
}
private boolean shouldShowSwLayerCutout(@BoundsPosition int pos,
@Nullable DisplayCutout cutout) {
final Rect[] bounds = cutout == null ? null : cutout.getBoundingRectsAll();
final int rotatedPos = getBoundPositionFromRotation(pos, mRotation);
return (bounds != null && !bounds[rotatedPos].isEmpty()
&& mHwcScreenDecorationSupport == null);
}
private boolean shouldOptimizeVisibility() {
return (isPrivacyDotEnabled() || mFaceScanningFactory.getHasProviders())
&& (mHwcScreenDecorationSupport != null
|| (!hasRoundedCorners() && !shouldDrawCutout())
);
}
private boolean shouldDrawCutout() {
return shouldDrawCutout(mContext);
}
static boolean shouldDrawCutout(Context context) {
return DisplayCutout.getFillBuiltInDisplayCutout(
context.getResources(), context.getDisplay().getUniqueId());
}
private void updateOverlayProviderViews() {
if (mOverlays == null) {
return;
}
++mProviderRefreshToken;
for (final OverlayWindow overlay: mOverlays) {
if (overlay == null) {
continue;
}
overlay.onReloadResAndMeasure(null, mProviderRefreshToken, mRotation, mDisplayUniqueId);
}
}
private void updateLayoutParams() {
//ToDo: We should skip unnecessary call to update view layout.
Trace.beginSection("ScreenDecorations#updateLayoutParams");
if (mScreenDecorHwcWindow != null) {
mWindowManager.updateViewLayout(mScreenDecorHwcWindow, getHwcWindowLayoutParams());
}
if (mOverlays != null) {
for (int i = 0; i < BOUNDS_POSITION_LENGTH; i++) {
if (mOverlays[i] == null) {
continue;
}
mWindowManager.updateViewLayout(
mOverlays[i].getRootView(), getWindowLayoutParams(i));
}
}
Trace.endSection();
}
@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 || !SIZE.equals(key)) {
return;
}
try {
final int sizeFactor = Integer.parseInt(newValue);
mRoundedCornerResDelegate.setTuningSizeFactor(sizeFactor);
} catch (NumberFormatException e) {
mRoundedCornerResDelegate.setTuningSizeFactor(null);
}
Integer[] filterIds = {
R.id.rounded_corner_top_left,
R.id.rounded_corner_top_right,
R.id.rounded_corner_bottom_left,
R.id.rounded_corner_bottom_right
};
for (final OverlayWindow overlay: mOverlays) {
if (overlay == null) {
continue;
}
overlay.onReloadResAndMeasure(filterIds, mProviderRefreshToken, mRotation,
mDisplayUniqueId);
}
updateHwLayerRoundedCornerExistAndSize();
});
}
private void updateHwLayerRoundedCornerDrawable() {
if (mScreenDecorHwcLayer == null) {
return;
}
final Drawable topDrawable = mRoundedCornerResDelegate.getTopRoundedDrawable();
final Drawable bottomDrawable = mRoundedCornerResDelegate.getBottomRoundedDrawable();
if (topDrawable == null || bottomDrawable == null) {
return;
}
mScreenDecorHwcLayer.updateRoundedCornerDrawable(topDrawable, bottomDrawable);
}
private void updateHwLayerRoundedCornerExistAndSize() {
if (mScreenDecorHwcLayer == null) {
return;
}
mScreenDecorHwcLayer.updateRoundedCornerExistenceAndSize(
mRoundedCornerResDelegate.getHasTop(),
mRoundedCornerResDelegate.getHasBottom(),
mRoundedCornerResDelegate.getTopRoundedSize().getWidth(),
mRoundedCornerResDelegate.getBottomRoundedSize().getWidth());
}
@VisibleForTesting
protected void setSize(View view, Size pixelSize) {
LayoutParams params = view.getLayoutParams();
params.width = pixelSize.getWidth();
params.height = pixelSize.getHeight();
view.setLayoutParams(params);
}
public static class DisplayCutoutView extends DisplayCutoutBaseView {
final List<Rect> mBounds = new ArrayList();
final Rect mBoundingRect = new Rect();
Rect mTotalBounds = new Rect();
private int mColor = Color.BLACK;
private int mRotation;
private int mInitialPosition;
private int mPosition;
public DisplayCutoutView(Context context, @BoundsPosition int pos) {
super(context);
mInitialPosition = pos;
paint.setColor(mColor);
paint.setStyle(Paint.Style.FILL);
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;
paint.setColor(mColor);
invalidate();
}
@Override
public void updateRotation(int rotation) {
mRotation = rotation;
super.updateRotation(rotation);
}
@VisibleForTesting(otherwise = VisibleForTesting.PROTECTED)
@Override
public void updateCutout() {
if (!isAttachedToWindow() || pendingConfigChange) {
return;
}
mPosition = getBoundPositionFromRotation(mInitialPosition, mRotation);
requestLayout();
getDisplay().getDisplayInfo(displayInfo);
mBounds.clear();
mBoundingRect.setEmpty();
cutoutPath.reset();
int newVisible;
if (shouldDrawCutout(getContext()) && hasCutout()) {
mBounds.addAll(displayInfo.displayCutout.getBoundingRects());
localBounds(mBoundingRect);
updateGravity();
updateBoundingPath();
invalidate();
newVisible = VISIBLE;
} else {
newVisible = GONE;
}
if (newVisible != getVisibility()) {
setVisibility(newVisible);
}
}
private void updateBoundingPath() {
final Path path = displayInfo.displayCutout.getCutoutPath();
if (path != null) {
cutoutPath.set(path);
} else {
cutoutPath.reset();
}
}
private void updateGravity() {
LayoutParams lp = getLayoutParams();
if (lp instanceof FrameLayout.LayoutParams) {
FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) lp;
int newGravity = getGravity(displayInfo.displayCutout);
if (flp.gravity != newGravity) {
flp.gravity = newGravity;
setLayoutParams(flp);
}
}
}
private boolean hasCutout() {
final DisplayCutout displayCutout = displayInfo.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 (showProtection) {
// Make sure that our measured height encompasses the protection
mTotalBounds.union(mBoundingRect);
mTotalBounds.union((int) protectionRect.left, (int) protectionRect.top,
(int) protectionRect.right, (int) protectionRect.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 = displayInfo.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;
}
}
/**
* 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 Display.Mode mTargetDisplayMode;
// Pass -1 for ScreenDecorHwcLayer since it's a fullscreen window and has no specific
// position.
private final int mPosition;
private RestartingPreDrawListener(View view, @BoundsPosition int position,
int targetRotation, Display.Mode targetDisplayMode) {
mView = view;
mTargetRotation = targetRotation;
mTargetDisplayMode = targetDisplayMode;
mPosition = position;
}
@Override
public boolean onPreDraw() {
mView.getViewTreeObserver().removeOnPreDrawListener(this);
if (mTargetRotation == mRotation
&& !displayModeChanged(mDisplayMode, mTargetDisplayMode)) {
if (DEBUG) {
final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
: getWindowTitleByPos(mPosition);
Log.i(TAG, title + " already in target rot "
+ mTargetRotation + " and in target resolution "
+ mTargetDisplayMode.getPhysicalWidth() + "x"
+ mTargetDisplayMode.getPhysicalHeight()
+ ", allow draw without restarting it");
}
return true;
}
mPendingConfigChange = false;
// This changes the window attributes - we need to restart the traversal for them to
// take effect.
updateConfiguration();
if (DEBUG) {
final String title = mPosition < 0 ? "ScreenDecorHwcLayer"
: getWindowTitleByPos(mPosition);
Log.i(TAG, title
+ " restarting listener fired, restarting draw for rot " + mRotation
+ ", resolution " + mDisplayMode.getPhysicalWidth() + "x"
+ mDisplayMode.getPhysicalHeight());
}
mView.invalidate();
return false;
}
}
/**
* A pre-draw listener, that validates that the rotation and display resolution we draw in
* matches the display's rotation and resolution 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() {
mContext.getDisplay().getDisplayInfo(mDisplayInfo);
final int displayRotation = mDisplayInfo.rotation;
final Display.Mode displayMode = mDisplayInfo.getMode();
if ((displayRotation != mRotation || displayModeChanged(mDisplayMode, displayMode))
&& !mPendingConfigChange) {
if (DEBUG) {
if (displayRotation != mRotation) {
Log.i(TAG, "Drawing rot " + mRotation + ", but display is at rot "
+ displayRotation + ". Restarting draw");
}
if (displayModeChanged(mDisplayMode, displayMode)) {
Log.i(TAG, "Drawing at " + mDisplayMode.getPhysicalWidth()
+ "x" + mDisplayMode.getPhysicalHeight() + ", but display is at "
+ displayMode.getPhysicalWidth() + "x"
+ displayMode.getPhysicalHeight() + ". Restarting draw");
}
}
mView.invalidate();
return false;
}
return true;
}
}
}