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

Commit 514e9b16 authored by Nate Myren's avatar Nate Myren Committed by Android (Google) Code Review
Browse files

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

parents 44211865 98dd5315
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
@@ -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>

+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