Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 98dd5315 authored by Evan Laird's avatar Evan Laird
Browse files

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
parent 3952307a
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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>
 No newline at end of file
+2 −1
Original line number Diff line number Diff line
@@ -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>
 No newline at end of file
+5 −0
Original line number Diff line number Diff line
@@ -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>

+0 −2
Original line number Diff line number Diff line
@@ -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;
+403 −0
Original line number Diff line number Diff line
/*
 * 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
}
Loading