Rotation-invariant status bar layout calculator

This CL implements a handful of methods that enable us to pre-emptively
calculate the position of the status bar, taking into account
DisplayCutout and rounded corner content padding.

The status bar is very special in that it always sits at the top of the
device, and has a known height, only taking into account window insets
with regard to its content layout. Therefore it can be known in all
orientations what the positions will be before we get a chance to lay
out the views on a configuration change.

Still to do: use this to inform Window Manager APIs, and use this
information to rework the privacy dot view controller not to require a
late publication of the status bar location

Test: manual
Bug: 187973222
Change-Id: Ieeba175065c522a0616f109b3670b752faef7f2a
This commit is contained in:
Evan Laird
2021-05-12 23:19:51 -04:00
parent 3952307a8c
commit 98dd5315aa
7 changed files with 526 additions and 8 deletions

View File

@@ -20,7 +20,7 @@
<solid
android:color="@color/privacy_chip_background"/>
<size
android:width="6dp"
android:height="6dp"
android:width="@dimen/ongoing_appops_dot_diameter"
android:height="@dimen/ongoing_appops_dot_diameter"
/>
</shape>

View File

@@ -32,6 +32,7 @@
android:paddingEnd="10dp"
android:gravity="center"
android:layout_gravity="center"
android:minWidth="56dp"
android:minWidth="@dimen/ongoing_appops_chip_min_width"
android:maxWidth="@dimen/ongoing_appops_chip_max_width"
/>
</com.android.systemui.privacy.OngoingPrivacyChip>

View File

@@ -1239,6 +1239,11 @@
<dimen name="ongoing_appops_chip_icon_size">16dp</dimen>
<!-- Radius of Ongoing App Ops chip corners -->
<dimen name="ongoing_appops_chip_bg_corner_radius">28dp</dimen>
<!-- One or two privacy items -->
<dimen name="ongoing_appops_chip_min_width">56dp</dimen>
<!-- Three privacy items. This value must not be exceeded -->
<dimen name="ongoing_appops_chip_max_width">76dp</dimen>
<dimen name="ongoing_appops_dot_diameter">6dp</dimen>
<dimen name="ongoing_appops_dialog_side_margins">@dimen/notification_shade_content_margin_horizontal</dimen>

View File

@@ -30,7 +30,6 @@ import android.annotation.Nullable;
import android.app.Fragment;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
@@ -230,7 +229,6 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
if (displayId != getContext().getDisplayId()) {
return;
}
Log.d(TAG, "disable: ");
state1 = adjustDisableFlags(state1);
final int old1 = mDisabled1;
final int diff1 = state1 ^ old1;

View File

@@ -0,0 +1,403 @@
/*
* Copyright (C) 2021 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.statusbar.phone
import android.content.Context
import android.content.res.Resources
import android.graphics.Rect
import android.util.Pair
import android.view.DisplayCutout
import android.view.View.LAYOUT_DIRECTION_RTL
import android.view.WindowManager
import com.android.systemui.Dumpable
import com.android.systemui.R
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.policy.CallbackController
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.util.leak.RotationUtils
import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE
import com.android.systemui.util.leak.RotationUtils.ROTATION_NONE
import com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE
import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN
import com.android.systemui.util.leak.RotationUtils.Rotation
import java.io.FileDescriptor
import java.io.PrintWriter
import java.lang.Math.max
import javax.inject.Inject
/**
* Encapsulates logic that can solve for the left/right insets required for the status bar contents.
* Takes into account:
* 1. rounded_corner_content_padding
* 2. status_bar_padding_start, status_bar_padding_end
* 2. display cutout insets from left or right
* 3. waterfall insets
*
*
* Importantly, these functions can determine status bar content left/right insets for any rotation
* before having done a layout pass in that rotation.
*
* NOTE: This class is not threadsafe
*/
@SysUISingleton
class StatusBarContentInsetsProvider @Inject constructor(
val context: Context,
val configurationController: ConfigurationController,
val windowManager: WindowManager,
val dumpManager: DumpManager
) : CallbackController<StatusBarContentInsetsChangedListener>,
ConfigurationController.ConfigurationListener,
Dumpable {
// Indexed by @Rotation
private val insetsByCorner = arrayOfNulls<Rect>(4)
private val listeners = mutableSetOf<StatusBarContentInsetsChangedListener>()
init {
configurationController.addCallback(this)
dumpManager.registerDumpable(TAG, this)
}
override fun addCallback(listener: StatusBarContentInsetsChangedListener) {
listeners.add(listener)
}
override fun removeCallback(listener: StatusBarContentInsetsChangedListener) {
listeners.remove(listener)
}
override fun onDensityOrFontScaleChanged() {
clearCachedInsets()
}
override fun onOverlayChanged() {
clearCachedInsets()
}
private fun clearCachedInsets() {
insetsByCorner[0] = null
insetsByCorner[1] = null
insetsByCorner[2] = null
insetsByCorner[3] = null
notifyInsetsChanged()
}
private fun notifyInsetsChanged() {
listeners.forEach {
it.onStatusBarContentInsetsChanged()
}
}
/**
* Calculates the maximum bounding rectangle for the privacy chip animation + ongoing privacy
* dot in the coordinates relative to the given rotation.
*/
fun getBoundingRectForPrivacyChipForRotation(@Rotation rotation: Int): Rect {
var insets = insetsByCorner[rotation]
val rotatedResources = RotationUtils.getResourcesForRotation(rotation, context)
if (insets == null) {
insets = getAndSetInsetsForRotation(rotation, rotatedResources)
}
val dotWidth = rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter)
val chipWidth = rotatedResources.getDimensionPixelSize(
R.dimen.ongoing_appops_chip_max_width)
return if (context.resources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL) {
Rect(insets.left - dotWidth,
insets.top,
insets.left + chipWidth,
insets.bottom)
} else {
Rect(insets.right - chipWidth,
insets.top,
insets.right + dotWidth,
insets.bottom)
}
}
/**
* Calculates the necessary left and right locations for the status bar contents invariant of
* the current device rotation, in the target rotation's coordinates
*/
fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Rect {
var insets = insetsByCorner[rotation]
if (insets == null) {
val rotatedResources = RotationUtils.getResourcesForRotation(rotation, context)
insets = getCalculatedInsetsForRotation(rotation, rotatedResources)
insetsByCorner[rotation] = insets
}
return insets
}
private fun getAndSetInsetsForRotation(
@Rotation rot: Int,
rotatedResources: Resources
): Rect {
val insets = getCalculatedInsetsForRotation(rot, rotatedResources)
insetsByCorner[rot] = insets
return insets
}
private fun getCalculatedInsetsForRotation(
@Rotation rotation: Int,
rotatedResources: Resources
): Rect {
val dc = context.display.cutout
return calculateInsetsForRotationWithRotatedResources(
rotation, rotatedResources, dc, windowManager, context)
}
override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) {
insetsByCorner.forEachIndexed { index, rect ->
pw.println("${RotationUtils.toString(index)} -> $rect")
}
}
}
interface StatusBarContentInsetsChangedListener {
fun onStatusBarContentInsetsChanged()
}
private const val TAG = "StatusBarInsetsProvider"
private fun getRotationZeroDisplayBounds(wm: WindowManager, @Rotation exactRotation: Int): Rect {
val bounds = wm.maximumWindowMetrics.bounds
if (exactRotation == ROTATION_NONE || exactRotation == ROTATION_UPSIDE_DOWN) {
return bounds
}
// bounds are horizontal, swap height and width
return Rect(0, 0, bounds.bottom, bounds.right)
}
private fun getCurrentDisplayBounds(wm: WindowManager): Rect {
val bounds = wm.maximumWindowMetrics.bounds
return bounds
}
/**
* Calculates the exact left and right positions for the status bar contents for the given
* rotation
*
* @param rot rotation for which to query the margins
* @param context systemui context
* @param rotatedResources resources constructed with the proper orientation set
*
* @see [RotationUtils#getResourcesForRotation]
*/
fun calculateInsetsForRotationWithRotatedResources(
@Rotation targetRotation: Int,
rotatedResources: Resources,
displayCutout: DisplayCutout?,
windowmanager: WindowManager,
context: Context
): Rect {
val rtl = rotatedResources.configuration.layoutDirection == LAYOUT_DIRECTION_RTL
val exactRotation = RotationUtils.getExactRotation(context)
val height = rotatedResources.getDimensionPixelSize(R.dimen.status_bar_height)
/*
TODO: Check if this is ever used for devices with no rounded corners
val paddingStart = rotatedResources.getDimensionPixelSize(R.dimen.status_bar_padding_start)
val paddingEnd = rotatedResources.getDimensionPixelSize(R.dimen.status_bar_padding_end)
val left = if (rtl) paddingEnd else paddingStart
val right = if(rtl) paddingStart else paddingEnd
*/
val roundedCornerPadding = rotatedResources.getDimensionPixelSize(
R.dimen.rounded_corner_content_padding)
val rotZeroBounds = getRotationZeroDisplayBounds(windowmanager, exactRotation)
val currentBounds = getCurrentDisplayBounds(windowmanager)
val sbLeftRight = getStatusBarLeftRight(
displayCutout,
height,
rotZeroBounds.right,
rotZeroBounds.bottom,
currentBounds.width(),
currentBounds.height(),
roundedCornerPadding,
targetRotation,
exactRotation)
return sbLeftRight
}
/**
* Calculate the insets needed from the left and right edges for the given rotation.
*
* @param dc Device display cutout
* @param sbHeight appropriate status bar height for this rotation
* @param width display width calculated for ROTATION_NONE
* @param height display height calculated for ROTATION_NONE
* @param roundedCornerPadding rounded_corner_content_padding dimension
* @param targetRotation the rotation for which to calculate margins
* @param currentRotation the rotation from which the display cutout was generated
*
* @return a Rect which exactly calculates the Status Bar's content rect relative to the target
* rotation
*/
private fun getStatusBarLeftRight(
dc: DisplayCutout?,
sbHeight: Int,
width: Int,
height: Int,
cWidth: Int,
cHeight: Int,
roundedCornerPadding: Int,
@Rotation targetRotation: Int,
@Rotation currentRotation: Int
): Rect {
val logicalDisplayWidth = if (targetRotation.isHorizontal()) height else width
val cutoutRects = dc?.boundingRects
if (cutoutRects == null || cutoutRects.isEmpty()) {
return Rect(roundedCornerPadding,
0,
logicalDisplayWidth - roundedCornerPadding,
sbHeight)
}
val relativeRotation = if (currentRotation - targetRotation < 0) {
currentRotation - targetRotation + 4
} else {
currentRotation - targetRotation
}
// Size of the status bar window for the given rotation relative to our exact rotation
val sbRect = sbRect(relativeRotation, sbHeight, Pair(cWidth, cHeight))
var leftMargin = roundedCornerPadding
var rightMargin = roundedCornerPadding
for (cutoutRect in cutoutRects) {
// There is at most one non-functional area per short edge of the device. So if the status
// bar doesn't share a short edge with the cutout, we can ignore its insets because there
// will be no letter-boxing to worry about
if (!shareShortEdge(sbRect, cutoutRect, cWidth, cHeight)) {
continue
}
if (cutoutRect.touchesLeftEdge(relativeRotation, cWidth, cHeight)) {
val l = max(roundedCornerPadding, cutoutRect.logicalWidth(relativeRotation))
leftMargin = max(l, leftMargin)
} else if (cutoutRect.touchesRightEdge(relativeRotation, cWidth, cHeight)) {
val logicalWidth = cutoutRect.logicalWidth(relativeRotation)
rightMargin = max(roundedCornerPadding, logicalWidth)
}
}
return Rect(leftMargin, 0, logicalDisplayWidth - rightMargin, sbHeight)
}
private fun sbRect(
@Rotation relativeRotation: Int,
sbHeight: Int,
displaySize: Pair<Int, Int>
): Rect {
val w = displaySize.first
val h = displaySize.second
return when (relativeRotation) {
ROTATION_NONE -> Rect(0, 0, w, sbHeight)
ROTATION_LANDSCAPE -> Rect(0, 0, sbHeight, h)
ROTATION_UPSIDE_DOWN -> Rect(0, h - sbHeight, w, h)
else -> Rect(w - sbHeight, 0, w, h)
}
}
private fun shareShortEdge(
sbRect: Rect,
cutoutRect: Rect,
currentWidth: Int,
currentHeight: Int
): Boolean {
if (currentWidth < currentHeight) {
// Check top/bottom edges by extending the width of the display cutout rect and checking
// for intersections
return sbRect.intersects(0, cutoutRect.top, currentWidth, cutoutRect.bottom)
} else if (currentWidth > currentHeight) {
// Short edge is the height, extend that one this time
return sbRect.intersects(cutoutRect.left, 0, cutoutRect.right, currentHeight)
}
return false
}
private fun Rect.touchesRightEdge(@Rotation rot: Int, width: Int, height: Int): Boolean {
return when (rot) {
ROTATION_NONE -> right >= width
ROTATION_LANDSCAPE -> top <= 0
ROTATION_UPSIDE_DOWN -> left <= 0
else /* SEASCAPE */ -> bottom >= height
}
}
private fun Rect.touchesLeftEdge(@Rotation rot: Int, width: Int, height: Int): Boolean {
return when (rot) {
ROTATION_NONE -> left <= 0
ROTATION_LANDSCAPE -> bottom >= height
ROTATION_UPSIDE_DOWN -> right >= width
else /* SEASCAPE */ -> top <= 0
}
}
private fun Rect.logicalTop(@Rotation rot: Int): Int {
return when (rot) {
ROTATION_NONE -> top
ROTATION_LANDSCAPE -> left
ROTATION_UPSIDE_DOWN -> bottom
else /* SEASCAPE */ -> right
}
}
private fun Rect.logicalRight(@Rotation rot: Int): Int {
return when (rot) {
ROTATION_NONE -> right
ROTATION_LANDSCAPE -> top
ROTATION_UPSIDE_DOWN -> left
else /* SEASCAPE */ -> bottom
}
}
private fun Rect.logicalLeft(@Rotation rot: Int): Int {
return when (rot) {
ROTATION_NONE -> left
ROTATION_LANDSCAPE -> bottom
ROTATION_UPSIDE_DOWN -> right
else /* SEASCAPE */ -> top
}
}
private fun Rect.logicalWidth(@Rotation rot: Int): Int {
return when (rot) {
ROTATION_NONE, ROTATION_UPSIDE_DOWN -> width()
else /* LANDSCAPE, SEASCAPE */ -> height()
}
}
private fun Int.isHorizontal(): Boolean {
return this == ROTATION_LANDSCAPE || this == ROTATION_SEASCAPE
}

View File

@@ -20,12 +20,20 @@ import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_M
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_COLOR_SPACE_AGNOSTIC;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_SHOW_STATUS_BAR;
import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE;
import static com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.os.Binder;
import android.os.RemoteException;
import android.util.Log;
import android.view.Gravity;
import android.view.IWindowManager;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -46,7 +54,9 @@ public class StatusBarWindowController {
private final Context mContext;
private final WindowManager mWindowManager;
private final IWindowManager mIWindowManager;
private final SuperStatusBarViewFactory mSuperStatusBarViewFactory;
private final StatusBarContentInsetsProvider mContentInsetsProvider;
private final Resources mResources;
private int mBarHeight = -1;
private final State mCurrentState = new State();
@@ -57,11 +67,17 @@ public class StatusBarWindowController {
private final WindowManager.LayoutParams mLpChanged;
@Inject
public StatusBarWindowController(Context context, WindowManager windowManager,
public StatusBarWindowController(
Context context,
WindowManager windowManager,
IWindowManager iWindowManager,
SuperStatusBarViewFactory superStatusBarViewFactory,
StatusBarContentInsetsProvider contentInsetsProvider,
@Main Resources resources) {
mContext = context;
mWindowManager = windowManager;
mIWindowManager = iWindowManager;
mContentInsetsProvider = contentInsetsProvider;
mSuperStatusBarViewFactory = superStatusBarViewFactory;
mStatusBarView = mSuperStatusBarViewFactory.getStatusBarWindowView();
mLaunchAnimationContainer = mStatusBarView.findViewById(
@@ -120,6 +136,27 @@ public class StatusBarWindowController {
mWindowManager.addView(mStatusBarView, mLp);
mLpChanged.copyFrom(mLp);
mContentInsetsProvider.addCallback(this::calculateStatusBarLocationsForAllRotations);
calculateStatusBarLocationsForAllRotations();
}
private void calculateStatusBarLocationsForAllRotations() {
Rect[] bounds = new Rect[4];
bounds[0] = mContentInsetsProvider
.getBoundingRectForPrivacyChipForRotation(ROTATION_NONE);
bounds[1] = mContentInsetsProvider
.getBoundingRectForPrivacyChipForRotation(ROTATION_LANDSCAPE);
bounds[2] = mContentInsetsProvider
.getBoundingRectForPrivacyChipForRotation(ROTATION_UPSIDE_DOWN);
bounds[3] = mContentInsetsProvider
.getBoundingRectForPrivacyChipForRotation(ROTATION_SEASCAPE);
try {
mIWindowManager.updateStaticPrivacyIndicatorBounds(mContext.getDisplayId(), bounds);
} catch (RemoteException e) {
//Swallow
}
}
/** Set force status bar visible. */

View File

@@ -14,16 +14,40 @@
package com.android.systemui.util.leak;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
import android.annotation.IntDef;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.view.Surface;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
public class RotationUtils {
public static final int ROTATION_NONE = 0;
public static final int ROTATION_LANDSCAPE = 1;
public static final int ROTATION_SEASCAPE = 2;
public static final int ROTATION_UPSIDE_DOWN = 3;
public static final int ROTATION_UPSIDE_DOWN = 2;
public static final int ROTATION_SEASCAPE = 3;
// Not to be confused with Surface.Rotation
@IntDef(prefix = { "ROTATION_" }, value = {
ROTATION_NONE,
ROTATION_LANDSCAPE,
ROTATION_SEASCAPE,
ROTATION_UPSIDE_DOWN,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Rotation {};
/**
* @return the current rotation, differentiating between none (rot_0), landscape (rot_90), and
* seascape (rot_180). upside down is not distinguished here
*/
@Rotation
public static int getRotation(Context context) {
int rot = context.getDisplay().getRotation();
if (rot == Surface.ROTATION_90) {
@@ -35,6 +59,11 @@ public class RotationUtils {
}
}
/**
* @return the current rotation, differentiating between landscape (rot_90), seascape
* (rot_270), and upside down (rot_180)
*/
@Rotation
public static int getExactRotation(Context context) {
int rot = context.getDisplay().getRotation();
if (rot == Surface.ROTATION_90) {
@@ -47,4 +76,49 @@ public class RotationUtils {
return ROTATION_NONE;
}
}
/** * To string */
public static String toString(@Rotation int rot) {
switch (rot) {
case ROTATION_NONE:
return "None (0)";
case ROTATION_LANDSCAPE:
return "Landscape (1)";
case ROTATION_UPSIDE_DOWN:
return "Upside down (2)";
case ROTATION_SEASCAPE:
return "Seascape (3)";
default:
return "Unknown (" + rot + ")";
}
}
/**
* Create a Resources using the specified rotation for the configuration. Use this to retrieve
* resources in values or values-land without needing an actual rotation to happen.
*
* @param rot the target rotation for which to create the resources
* @param context a context
* @return a Resources object configured for the given orientation
*/
public static Resources getResourcesForRotation(@Rotation int rot, Context context) {
int orientation;
switch (rot) {
case ROTATION_NONE:
case ROTATION_UPSIDE_DOWN:
orientation = ORIENTATION_PORTRAIT;
break;
case ROTATION_LANDSCAPE:
case ROTATION_SEASCAPE:
orientation = ORIENTATION_LANDSCAPE;
break;
default:
throw new IllegalArgumentException("Unknown rotation: " + rot);
}
Configuration c = context.getResources().getConfiguration();
c.orientation = orientation;
Context rotated = context.createConfigurationContext(c);
return rotated.getResources();
}
}