diff --git a/packages/SystemUI/res/drawable/system_animation_ongoing_dot.xml b/packages/SystemUI/res/drawable/system_animation_ongoing_dot.xml index 9f81b0d59973f..09af6d2dace7e 100644 --- a/packages/SystemUI/res/drawable/system_animation_ongoing_dot.xml +++ b/packages/SystemUI/res/drawable/system_animation_ongoing_dot.xml @@ -20,7 +20,7 @@ \ No newline at end of file diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml index 676e49298cefe..812277634d094 100644 --- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml +++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml @@ -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" /> \ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 915b62791f585..7b821eb7e2bbe 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1239,6 +1239,11 @@ 16dp 28dp + + 56dp + + 76dp + 6dp @dimen/notification_shade_content_margin_horizontal diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java index 684760e6c3a3d..69360b290118a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CollapsedStatusBarFragment.java @@ -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; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt new file mode 100644 index 0000000000000..8befe800b4e1d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt @@ -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, + ConfigurationController.ConfigurationListener, + Dumpable { + // Indexed by @Rotation + private val insetsByCorner = arrayOfNulls(4) + private val listeners = mutableSetOf() + + 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) { + 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 +): 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 +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java index 30b8c5c0d8d34..9a25a70788592 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarWindowController.java @@ -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. */ diff --git a/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java b/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java index e44981e249064..b9b7730c67f3b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java +++ b/packages/SystemUI/src/com/android/systemui/util/leak/RotationUtils.java @@ -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(); + } }