Merge "Rotation-invariant status bar layout calculator" into sc-dev

This commit is contained in:
Nate Myren
2021-05-22 00:13:34 +00:00
committed by Android (Google) Code Review
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

@@ -1243,6 +1243,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();
}
}