| /* |
| * 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.Point |
| import android.graphics.Rect |
| import android.util.LruCache |
| import android.util.Pair |
| import android.view.DisplayCutout |
| |
| import androidx.annotation.VisibleForTesting |
| |
| import com.android.internal.policy.SystemBarUtils |
| 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.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 com.android.systemui.util.leak.RotationUtils.getExactRotation |
| import com.android.systemui.util.leak.RotationUtils.getResourcesForRotation |
| |
| 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 dumpManager: DumpManager |
| ) : CallbackController<StatusBarContentInsetsChangedListener>, |
| ConfigurationController.ConfigurationListener, |
| Dumpable { |
| |
| // Limit cache size as potentially we may connect large number of displays |
| // (e.g. network displays) |
| private val insetsCache = LruCache<CacheKey, Rect>(MAX_CACHE_SIZE) |
| private val listeners = mutableSetOf<StatusBarContentInsetsChangedListener>() |
| private val isPrivacyDotEnabled: Boolean by lazy(LazyThreadSafetyMode.PUBLICATION) { |
| context.resources.getBoolean(R.bool.config_enablePrivacyDot) |
| } |
| |
| 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 onThemeChanged() { |
| clearCachedInsets() |
| } |
| |
| override fun onMaxBoundsChanged() { |
| notifyInsetsChanged() |
| } |
| |
| private fun clearCachedInsets() { |
| insetsCache.evictAll() |
| notifyInsetsChanged() |
| } |
| |
| private fun notifyInsetsChanged() { |
| listeners.forEach { |
| it.onStatusBarContentInsetsChanged() |
| } |
| } |
| |
| /** |
| * Some views may need to care about whether or not the current top display cutout is located |
| * in the corner rather than somewhere in the center. In the case of a corner cutout, the |
| * status bar area is contiguous. |
| */ |
| fun currentRotationHasCornerCutout(): Boolean { |
| val cutout = context.display.cutout ?: return false |
| val topBounds = cutout.boundingRectTop |
| |
| val point = Point() |
| context.display.getRealSize(point) |
| |
| return topBounds.left <= 0 || topBounds.right >= point.y |
| } |
| |
| /** |
| * Calculates the maximum bounding rectangle for the privacy chip animation + ongoing privacy |
| * dot in the coordinates relative to the given rotation. |
| * |
| * @param rotation the rotation for which the bounds are required. This is an absolute value |
| * (i.e., ROTATION_NONE will always return the same bounds regardless of the context |
| * from which this method is called) |
| */ |
| fun getBoundingRectForPrivacyChipForRotation(@Rotation rotation: Int): Rect { |
| var insets = insetsCache[getCacheKey(rotation = rotation)] |
| if (insets == null) { |
| insets = getStatusBarContentAreaForRotation(rotation) |
| } |
| |
| val rotatedResources = getResourcesForRotation(rotation, context) |
| |
| val dotWidth = rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter) |
| val chipWidth = rotatedResources.getDimensionPixelSize( |
| R.dimen.ongoing_appops_chip_max_width) |
| |
| val isRtl = configurationController.isLayoutRtl |
| return getPrivacyChipBoundingRectForInsets(insets, dotWidth, chipWidth, isRtl) |
| } |
| |
| /** |
| * Calculate the distance from the left and right edges of the screen to the status bar |
| * content area. This differs from the content area rects in that these values can be used |
| * directly as padding. |
| * |
| * @param rotation the target rotation for which to calculate insets |
| */ |
| fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Pair<Int, Int> { |
| val key = getCacheKey(rotation) |
| |
| val point = Point() |
| context.display.getRealSize(point) |
| // Target rotation can be a different orientation than the current device rotation |
| point.orientToRotZero(getExactRotation(context)) |
| val width = point.logicalWidth(rotation) |
| |
| val area = insetsCache[key] ?: getAndSetCalculatedAreaForRotation( |
| rotation, getResourcesForRotation(rotation, context), key) |
| |
| return Pair(area.left, width - area.right) |
| } |
| |
| /** |
| * Calculate the left and right insets for the status bar content in the device's current |
| * rotation |
| * @see getStatusBarContentAreaForRotation |
| */ |
| fun getStatusBarContentInsetsForCurrentRotation(): Pair<Int, Int> { |
| return getStatusBarContentInsetsForRotation(getExactRotation(context)) |
| } |
| |
| /** |
| * Calculates the area of the status bar contents invariant of the current device rotation, |
| * in the target rotation's coordinates |
| * |
| * @param rotation the rotation for which the bounds are required. This is an absolute value |
| * (i.e., ROTATION_NONE will always return the same bounds regardless of the context |
| * from which this method is called) |
| */ |
| @JvmOverloads |
| fun getStatusBarContentAreaForRotation( |
| @Rotation rotation: Int |
| ): Rect { |
| val key = getCacheKey(rotation) |
| return insetsCache[key] ?: getAndSetCalculatedAreaForRotation( |
| rotation, getResourcesForRotation(rotation, context), key) |
| } |
| |
| /** |
| * Get the status bar content area for the given rotation, in absolute bounds |
| */ |
| fun getStatusBarContentAreaForCurrentRotation(): Rect { |
| val rotation = getExactRotation(context) |
| return getStatusBarContentAreaForRotation(rotation) |
| } |
| |
| private fun getAndSetCalculatedAreaForRotation( |
| @Rotation targetRotation: Int, |
| rotatedResources: Resources, |
| key: CacheKey |
| ): Rect { |
| return getCalculatedAreaForRotation(targetRotation, rotatedResources) |
| .also { |
| insetsCache.put(key, it) |
| } |
| } |
| |
| private fun getCalculatedAreaForRotation( |
| @Rotation targetRotation: Int, |
| rotatedResources: Resources |
| ): Rect { |
| val dc = context.display.cutout |
| val currentRotation = getExactRotation(context) |
| |
| val roundedCornerPadding = rotatedResources |
| .getDimensionPixelSize(R.dimen.rounded_corner_content_padding) |
| val minDotPadding = if (isPrivacyDotEnabled) |
| rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_min_padding) |
| else 0 |
| val dotWidth = if (isPrivacyDotEnabled) |
| rotatedResources.getDimensionPixelSize(R.dimen.ongoing_appops_dot_diameter) |
| else 0 |
| |
| val minLeft: Int |
| val minRight: Int |
| if (configurationController.isLayoutRtl) { |
| minLeft = max(minDotPadding, roundedCornerPadding) |
| minRight = roundedCornerPadding |
| } else { |
| minLeft = roundedCornerPadding |
| minRight = max(minDotPadding, roundedCornerPadding) |
| } |
| |
| return calculateInsetsForRotationWithRotatedResources( |
| currentRotation, |
| targetRotation, |
| dc, |
| context.resources.configuration.windowConfiguration.maxBounds, |
| SystemBarUtils.getStatusBarHeightForRotation(context, targetRotation), |
| minLeft, |
| minRight, |
| configurationController.isLayoutRtl, |
| dotWidth) |
| } |
| |
| fun getStatusBarPaddingTop(@Rotation rotation: Int? = null): Int { |
| val res = rotation?.let { it -> getResourcesForRotation(it, context) } ?: context.resources |
| return res.getDimensionPixelSize(R.dimen.status_bar_padding_top) |
| } |
| |
| override fun dump(fd: FileDescriptor, pw: PrintWriter, args: Array<out String>) { |
| insetsCache.snapshot().forEach { (key, rect) -> |
| pw.println("$key -> $rect") |
| } |
| pw.println(insetsCache) |
| } |
| |
| private fun getCacheKey(@Rotation rotation: Int): CacheKey = |
| CacheKey( |
| uniqueDisplayId = context.display.uniqueId, |
| rotation = rotation |
| ) |
| |
| private data class CacheKey( |
| val uniqueDisplayId: String, |
| @Rotation val rotation: Int |
| ) |
| } |
| |
| interface StatusBarContentInsetsChangedListener { |
| fun onStatusBarContentInsetsChanged() |
| } |
| |
| private const val TAG = "StatusBarInsetsProvider" |
| private const val MAX_CACHE_SIZE = 16 |
| |
| private fun getRotationZeroDisplayBounds(bounds: Rect, @Rotation exactRotation: Int): Rect { |
| 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) |
| } |
| |
| @VisibleForTesting |
| fun getPrivacyChipBoundingRectForInsets( |
| contentRect: Rect, |
| dotWidth: Int, |
| chipWidth: Int, |
| isRtl: Boolean |
| ): Rect { |
| return if (isRtl) { |
| Rect(contentRect.left - dotWidth, |
| contentRect.top, |
| contentRect.left + chipWidth, |
| contentRect.bottom) |
| } else { |
| Rect(contentRect.right - chipWidth, |
| contentRect.top, |
| contentRect.right + dotWidth, |
| contentRect.bottom) |
| } |
| } |
| |
| /** |
| * Calculates the exact left and right positions for the status bar contents for the given |
| * rotation |
| * |
| * @param currentRotation current device rotation |
| * @param targetRotation rotation for which to calculate the status bar content rect |
| * @param displayCutout [DisplayCutout] for the current display. possibly null |
| * @param maxBounds the display bounds in our current rotation |
| * @param statusBarHeight height of the status bar for the target rotation |
| * @param minLeft the minimum padding to enforce on the left |
| * @param minRight the minimum padding to enforce on the right |
| * @param isRtl current layout direction is Right-To-Left or not |
| * @param dotWidth privacy dot image width (0 if privacy dot is disabled) |
| * |
| * @see [RotationUtils#getResourcesForRotation] |
| */ |
| fun calculateInsetsForRotationWithRotatedResources( |
| @Rotation currentRotation: Int, |
| @Rotation targetRotation: Int, |
| displayCutout: DisplayCutout?, |
| maxBounds: Rect, |
| statusBarHeight: Int, |
| minLeft: Int, |
| minRight: Int, |
| isRtl: Boolean, |
| dotWidth: Int |
| ): Rect { |
| /* |
| TODO: Check if this is ever used for devices with no rounded corners |
| val left = if (isRtl) paddingEnd else paddingStart |
| val right = if (isRtl) paddingStart else paddingEnd |
| */ |
| |
| val rotZeroBounds = getRotationZeroDisplayBounds(maxBounds, currentRotation) |
| |
| val sbLeftRight = getStatusBarLeftRight( |
| displayCutout, |
| statusBarHeight, |
| rotZeroBounds.right, |
| rotZeroBounds.bottom, |
| maxBounds.width(), |
| maxBounds.height(), |
| minLeft, |
| minRight, |
| isRtl, |
| dotWidth, |
| targetRotation, |
| currentRotation) |
| |
| 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 cWidth display width in our current rotation |
| * @param cHeight display height in our current rotation |
| * @param minLeft the minimum padding to enforce on the left |
| * @param minRight the minimum padding to enforce on the right |
| * @param isRtl current layout direction is Right-To-Left or not |
| * @param dotWidth privacy dot image width (0 if privacy dot is disabled) |
| * @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, |
| minLeft: Int, |
| minRight: Int, |
| isRtl: Boolean, |
| dotWidth: 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(minLeft, |
| 0, |
| logicalDisplayWidth - minRight, |
| 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 = minLeft |
| var rightMargin = minRight |
| 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)) { |
| var logicalWidth = cutoutRect.logicalWidth(relativeRotation) |
| if (isRtl) logicalWidth += dotWidth |
| leftMargin = max(logicalWidth, leftMargin) |
| } else if (cutoutRect.touchesRightEdge(relativeRotation, cWidth, cHeight)) { |
| var logicalWidth = cutoutRect.logicalWidth(relativeRotation) |
| if (!isRtl) logicalWidth += dotWidth |
| rightMargin = max(rightMargin, logicalWidth) |
| } |
| // TODO(b/203626889): Fix the scenario when config_mainBuiltInDisplayCutoutRectApproximation |
| // is very close to but not directly touch edges. |
| } |
| |
| 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 |
| } |
| |
| private fun Point.orientToRotZero(@Rotation rot: Int) { |
| when (rot) { |
| ROTATION_NONE, ROTATION_UPSIDE_DOWN -> return |
| else -> { |
| // swap width and height to zero-orient bounds |
| val yTmp = y |
| y = x |
| x = yTmp |
| } |
| } |
| } |
| |
| private fun Point.logicalWidth(@Rotation rot: Int): Int { |
| return when (rot) { |
| ROTATION_NONE, ROTATION_UPSIDE_DOWN -> x |
| else -> y |
| } |
| } |