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

Commit 3162fb4e authored by burakov's avatar burakov
Browse files

Improve NSSL `start` positioning for Dual Shade & Connected Displays.

So far, NSSL's start edge was positioned either at the beginning of the
screen (single shade) or at the half-width (split shade). Dual Shade
requires more fine-grained positioning, tied to the width of the shade
itself; the NSSL should be full-width on narrow screens, and a smaller
portion of the screen (either fixed size or a percentage) on wider
screens.

Summary of changes in this CL:

1. Define dual shade dimens in resource configuration files, instead of
   code constants. The final values for each config are still TBD, but
   the tentative spec is:
   * Pixel portrait/landscape: 412dp
   * Folded foldable portrait/landscape: 412dp
   * Unfolded foldable portrait/landscape: 392dp
   * Tablet portrait/landscape: 474dp

2. Replace `useSplitShade` with `horizontalPosition`. For unflagged
   code, this is a mechanical refactor where `useSplitShade == true`
   is equivalent to
   `horizontalPosition is HorizontalPosition.MiddleToEdge`. I added
   dedicated unit test cases to verify this.

3. Change the guideline percentage to match the ratio specified in
   `HorizontalPosition.MiddleToEdge`. This is still 50% at the moment,
   but will likely change soon for some Dual Shade configurations.

4. Under `HorizontalPosition.FloatAtEnd` (only enabled in SceneContainer
   and DualShade), do not constrain the NSSL start edge and instead
   constrain the width. This achieves a fixed-width, end-aligned panel.

Bug: 338033836
Flag: com.android.systemui.scene_container
Test: Verified manually by observing that notifications placement is
 unaffected on SceneContainer mode without dual shade, and narrower than
 half-screen when dual shade is enabled.
Test: Added unit tests.
Test: Existing unit tests still pass.

Change-Id: Ia2b03a3c0376c8490e15c8238ecd89a2eb7dae4f
parent d8181f25
Loading
Loading
Loading
Loading
+6 −8
Original line number Diff line number Diff line
@@ -45,11 +45,13 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.dp
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexContentPicker
import com.android.compose.animation.scene.SceneScope
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.res.R

/** Renders a lightweight shade UI container, as an overlay. */
@Composable
@@ -104,13 +106,11 @@ private fun SceneScope.Panel(modifier: Modifier = Modifier, content: @Composable
@Composable
private fun Modifier.panelSize(): Modifier {
    val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass

    return this.then(
        when (widthSizeClass) {
            WindowWidthSizeClass.Compact -> Modifier.fillMaxWidth()
            WindowWidthSizeClass.Medium -> Modifier.width(OverlayShade.Dimensions.PanelWidthMedium)
            WindowWidthSizeClass.Expanded -> Modifier.width(OverlayShade.Dimensions.PanelWidthLarge)
            else -> error("Unsupported WindowWidthSizeClass \"$widthSizeClass\"")
        if (widthSizeClass == WindowWidthSizeClass.Compact) {
            Modifier.fillMaxWidth()
        } else {
            Modifier.width(dimensionResource(id = R.dimen.shade_panel_width))
        }
    )
}
@@ -176,8 +176,6 @@ object OverlayShade {
    object Dimensions {
        val ScrimContentPadding = 16.dp
        val PanelCornerRadius = 46.dp
        val PanelWidthMedium = 390.dp
        val PanelWidthLarge = 474.dp
        val OverscrollLimit = 32.dp
    }

+81 −0
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@

package com.android.systemui.statusbar.notification.stack.ui.viewmodel

import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.FlagsParameterization
import androidx.test.filters.SmallTest
@@ -66,10 +67,13 @@ import com.android.systemui.scene.data.repository.setTransition
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.mockLargeScreenHeaderHelper
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.shade.shared.flag.DualShade
import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition
import com.android.systemui.testKosmos
import com.google.common.collect.Range
import com.google.common.truth.Truth.assertThat
import kotlin.test.assertIs
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
@@ -164,6 +168,83 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S
            assertThat(dimens!!.marginStart).isEqualTo(20)
        }

    @Test
    fun validateHorizontalPositionSingleShade() =
        testScope.runTest {
            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
            val dimens by collectLastValue(underTest.configurationBasedDimensions)
            shadeTestUtil.setSplitShade(false)

            val horizontalPosition = checkNotNull(dimens).horizontalPosition
            assertIs<HorizontalPosition.EdgeToEdge>(horizontalPosition)
        }

    @Test
    fun validateHorizontalPositionSplitShade() =
        testScope.runTest {
            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
            val dimens by collectLastValue(underTest.configurationBasedDimensions)
            shadeTestUtil.setSplitShade(true)

            val horizontalPosition = checkNotNull(dimens).horizontalPosition
            assertIs<HorizontalPosition.MiddleToEdge>(horizontalPosition)
            assertThat(horizontalPosition.ratio).isEqualTo(0.5f)
        }

    @Test
    @EnableSceneContainer
    @DisableFlags(DualShade.FLAG_NAME)
    fun validateHorizontalPositionInSceneContainerSingleShade() =
        testScope.runTest {
            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
            val dimens by collectLastValue(underTest.configurationBasedDimensions)
            shadeTestUtil.setSplitShade(false)

            val horizontalPosition = checkNotNull(dimens).horizontalPosition
            assertIs<HorizontalPosition.EdgeToEdge>(horizontalPosition)
        }

    @Test
    @EnableSceneContainer
    @DisableFlags(DualShade.FLAG_NAME)
    fun validateHorizontalPositionInSceneContainerSplitShade() =
        testScope.runTest {
            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
            val dimens by collectLastValue(underTest.configurationBasedDimensions)
            shadeTestUtil.setSplitShade(true)

            val horizontalPosition = checkNotNull(dimens).horizontalPosition
            assertIs<HorizontalPosition.MiddleToEdge>(horizontalPosition)
            assertThat(horizontalPosition.ratio).isEqualTo(0.5f)
        }

    @Test
    @EnableSceneContainer
    @EnableFlags(DualShade.FLAG_NAME)
    fun validateHorizontalPositionInDualShade_narrowLayout() =
        testScope.runTest {
            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
            val dimens by collectLastValue(underTest.configurationBasedDimensions)
            shadeTestUtil.setSplitShade(false)

            val horizontalPosition = checkNotNull(dimens).horizontalPosition
            assertIs<HorizontalPosition.EdgeToEdge>(horizontalPosition)
        }

    @Test
    @EnableSceneContainer
    @EnableFlags(DualShade.FLAG_NAME)
    fun validateHorizontalPositionInDualShade_wideLayout() =
        testScope.runTest {
            overrideDimensionPixelSize(R.dimen.shade_panel_width, 200)
            val dimens by collectLastValue(underTest.configurationBasedDimensions)
            shadeTestUtil.setSplitShade(true)

            val horizontalPosition = checkNotNull(dimens).horizontalPosition
            assertIs<HorizontalPosition.FloatAtEnd>(horizontalPosition)
            assertThat(horizontalPosition.width).isEqualTo(200)
        }

    @Test
    fun validatePaddingTopInSplitShade_usesLargeHeaderHelper() =
        testScope.runTest {
+3 −0
Original line number Diff line number Diff line
@@ -23,4 +23,7 @@
    <dimen name="keyguard_clock_top_margin">80dp</dimen>
    <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">155dp</dimen>
    <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">85dp</dimen>

    <!-- The width of the shade overlay panel (both notifications and quick settings). -->
    <dimen name="shade_panel_width">474dp</dimen>
</resources>
+3 −0
Original line number Diff line number Diff line
@@ -21,4 +21,7 @@

    <!-- Biometric Auth pattern view size, better to align keyguard_security_width -->
    <dimen name="biometric_auth_pattern_view_size">348dp</dimen>

    <!-- The width of the shade overlay panel (both notifications and quick settings). -->
    <dimen name="shade_panel_width">392dp</dimen>
</resources>
+20 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?><!--
  ~ Copyright (C) 2024 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.
  -->

<resources>
    <!-- The width of the shade overlay panel (both notifications and quick settings). -->
    <dimen name="shade_panel_width">474dp</dimen>
</resources>
Loading