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

Commit b6b1b262 authored by Evan Laird's avatar Evan Laird Committed by Automerger Merge Worker
Browse files

Merge "[Sb chip] Fix chip layout when fullscreen or rotating" into udc-qpr-dev...

Merge "[Sb chip] Fix chip layout when fullscreen or rotating" into udc-qpr-dev am: 811ef53b am: 2111aa47

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/23874234



Change-Id: Icfe04bffae8295e479a8a6a4bc7444b139586ad7
Signed-off-by: default avatarAutomerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
parents 633a9ddf 2111aa47
Loading
Loading
Loading
Loading
+61 −23
Original line number Diff line number Diff line
@@ -30,9 +30,11 @@ import androidx.core.animation.Animator
import androidx.core.animation.AnimatorListenerAdapter
import androidx.core.animation.AnimatorSet
import androidx.core.animation.ValueAnimator
import com.android.internal.annotations.VisibleForTesting
import com.android.systemui.R
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.animation.AnimationUtil.Companion.frames
@@ -46,7 +48,7 @@ class SystemEventChipAnimationController @Inject constructor(
    private val context: Context,
    private val statusBarWindowController: StatusBarWindowController,
    private val contentInsetsProvider: StatusBarContentInsetsProvider,
    private val featureFlags: FeatureFlags
    private val featureFlags: FeatureFlags,
) : SystemStatusAnimationCallback {

    private lateinit var animationWindowView: FrameLayout
@@ -56,7 +58,8 @@ class SystemEventChipAnimationController @Inject constructor(

    // Left for LTR, Right for RTL
    private var animationDirection = LEFT
    private var chipBounds = Rect()

    @VisibleForTesting var chipBounds = Rect()
    private val chipWidth get() = chipBounds.width()
    private val chipRight get() = chipBounds.right
    private val chipLeft get() = chipBounds.left
@@ -69,7 +72,7 @@ class SystemEventChipAnimationController @Inject constructor(
    private var animRect = Rect()

    // TODO: move to dagger
    private var initialized = false
    @VisibleForTesting var initialized = false

    /**
     * Give the chip controller a chance to inflate and configure the chip view before we start
@@ -98,23 +101,7 @@ class SystemEventChipAnimationController @Inject constructor(
                    View.MeasureSpec.makeMeasureSpec(
                            (animationWindowView.parent as View).height, AT_MOST))

            // decide which direction we're animating from, and then set some screen coordinates
            val contentRect = contentInsetsProvider.getStatusBarContentAreaForCurrentRotation()
            val chipTop = ((animationWindowView.parent as View).height - it.view.measuredHeight) / 2
            val chipBottom = chipTop + it.view.measuredHeight
            val chipRight: Int
            val chipLeft: Int
            when (animationDirection) {
                LEFT -> {
                    chipRight = contentRect.right
                    chipLeft = contentRect.right - it.chipWidth
                }
                else /* RIGHT */ -> {
                    chipLeft = contentRect.left
                    chipRight = contentRect.left + it.chipWidth
                }
            }
            chipBounds = Rect(chipLeft, chipTop, chipRight, chipBottom)
            updateChipBounds(it, contentInsetsProvider.getStatusBarContentAreaForCurrentRotation())
        }
    }

@@ -253,16 +240,67 @@ class SystemEventChipAnimationController @Inject constructor(
        return animSet
    }

    private fun init() {
    fun init() {
        initialized = true
        themedContext = ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)
        animationWindowView = LayoutInflater.from(themedContext)
                .inflate(R.layout.system_event_animation_window, null) as FrameLayout
        val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
        lp.gravity = Gravity.END or Gravity.CENTER_VERTICAL
        // Matches status_bar.xml
        val height = themedContext.resources.getDimensionPixelSize(R.dimen.status_bar_height)
        val lp = FrameLayout.LayoutParams(MATCH_PARENT, height)
        lp.gravity = Gravity.END or Gravity.TOP
        statusBarWindowController.addViewToWindow(animationWindowView, lp)
        animationWindowView.clipToPadding = false
        animationWindowView.clipChildren = false

        // Use contentInsetsProvider rather than configuration controller, since we only care
        // about status bar dimens
        contentInsetsProvider.addCallback(object : StatusBarContentInsetsChangedListener {
            override fun onStatusBarContentInsetsChanged() {
                val newContentArea = contentInsetsProvider
                    .getStatusBarContentAreaForCurrentRotation()
                updateDimens(newContentArea)

                // If we are currently animating, we have to re-solve for the chip bounds. If we're
                // not animating then [prepareChipAnimation] will take care of it for us
                currentAnimatedView?.let {
                    updateChipBounds(it, newContentArea)
                }
            }
        })
    }

    private fun updateDimens(contentArea: Rect) {
        val lp = animationWindowView.layoutParams as FrameLayout.LayoutParams
        lp.height = contentArea.height()

        animationWindowView.layoutParams = lp
    }

    /**
     * Use the current status bar content area and the current chip's measured size to update
     * the animation rect and chipBounds. This method can be called at any time and will update
     * the current animation values properly during e.g. a rotation.
     */
    private fun updateChipBounds(chip: BackgroundAnimatableView, contentArea: Rect) {
        // decide which direction we're animating from, and then set some screen coordinates
        val chipTop = (contentArea.bottom - chip.view.measuredHeight) / 2
        val chipBottom = chipTop + chip.view.measuredHeight
        val chipRight: Int
        val chipLeft: Int

        when (animationDirection) {
            LEFT -> {
                chipRight = contentArea.right
                chipLeft = contentArea.right - chip.chipWidth
            }
            else /* RIGHT */ -> {
                chipLeft = contentArea.left
                chipRight = contentArea.left + chip.chipWidth
            }
        }
        chipBounds = Rect(chipLeft, chipTop, chipRight, chipBottom)
        animRect.set(chipBounds)
    }

    private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams =
+189 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.events

import android.content.Context
import android.graphics.Rect
import android.util.Pair
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener
import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@SmallTest
class SystemEventChipAnimationControllerTest : SysuiTestCase() {
    private lateinit var controller: SystemEventChipAnimationController

    @Mock private lateinit var sbWindowController: StatusBarWindowController
    @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider

    private var testView = TestView(mContext)
    private var viewCreator: ViewCreator = { testView }

    @Before
    fun setup() {
        MockitoAnnotations.initMocks(this)

        // StatusBarWindowController is mocked. The addViewToWindow function needs to be mocked to
        // ensure that the chip view is added to a parent view
        whenever(sbWindowController.addViewToWindow(any(), any())).then {
            val statusbarFake = FrameLayout(mContext)
            statusbarFake.layout(
                portraitArea.left,
                portraitArea.top,
                portraitArea.right,
                portraitArea.bottom,
            )
            statusbarFake.addView(
                it.arguments[0] as View,
                it.arguments[1] as FrameLayout.LayoutParams
            )
        }

        whenever(insetsProvider.getStatusBarContentInsetsForCurrentRotation())
            .thenReturn(Pair(insets, insets))
        whenever(insetsProvider.getStatusBarContentAreaForCurrentRotation())
            .thenReturn(portraitArea)

        controller =
            SystemEventChipAnimationController(
                context = mContext,
                statusBarWindowController = sbWindowController,
                contentInsetsProvider = insetsProvider,
                featureFlags = FakeFeatureFlags(),
            )
    }

    @Test
    fun prepareChipAnimation_lazyInitializes() {
        // Until Dagger can do our initialization, make sure that the first chip animation calls
        // init()
        assertFalse(controller.initialized)
        controller.prepareChipAnimation(viewCreator)
        assertTrue(controller.initialized)
    }

    @Test
    fun prepareChipAnimation_positionsChip() {
        controller.prepareChipAnimation(viewCreator)
        val chipRect = controller.chipBounds

        // SB area = 10, 0, 990, 100
        // chip size = 0, 0, 100, 50
        assertThat(chipRect).isEqualTo(Rect(890, 25, 990, 75))
    }

    @Test
    fun prepareChipAnimation_rotation_repositionsChip() {
        controller.prepareChipAnimation(viewCreator)

        // Chip has been prepared, and is located at (890, 25, 990, 75)
        // Rotation should put it into its landscape location:
        // SB area = 10, 0, 1990, 80
        // chip size = 0, 0, 100, 50

        whenever(insetsProvider.getStatusBarContentAreaForCurrentRotation())
            .thenReturn(landscapeArea)
        getInsetsListener().onStatusBarContentInsetsChanged()

        val chipRect = controller.chipBounds
        assertThat(chipRect).isEqualTo(Rect(1890, 15, 1990, 65))
    }

    /** regression test for (b/289378932) */
    @Test
    fun fullScreenStatusBar_positionsChipAtTop_withTopGravity() {
        // In the case of a fullscreen status bar window, the content insets area is still correct
        // (because it uses the dimens), but the window can be full screen. This seems to happen
        // when launching an app from the ongoing call chip.

        // GIVEN layout the status bar window fullscreen portrait
        whenever(sbWindowController.addViewToWindow(any(), any())).then {
            val statusbarFake = FrameLayout(mContext)
            statusbarFake.layout(
                fullScreenSb.left,
                fullScreenSb.top,
                fullScreenSb.right,
                fullScreenSb.bottom,
            )

            val lp = it.arguments[1] as FrameLayout.LayoutParams
            assertThat(lp.gravity and Gravity.VERTICAL_GRAVITY_MASK).isEqualTo(Gravity.TOP)

            statusbarFake.addView(
                it.arguments[0] as View,
                lp,
            )
        }

        // GIVEN insets provider gives the correct content area
        whenever(insetsProvider.getStatusBarContentAreaForCurrentRotation())
            .thenReturn(portraitArea)

        // WHEN the controller lays out the chip in a fullscreen window
        controller.prepareChipAnimation(viewCreator)

        // THEN it still aligns the chip to the content area provided by the insets provider
        val chipRect = controller.chipBounds
        assertThat(chipRect).isEqualTo(Rect(890, 25, 990, 75))
    }

    class TestView(context: Context) : View(context), BackgroundAnimatableView {
        override val view: View
            get() = this

        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            setMeasuredDimension(100, 50)
        }

        override fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) {
            setLeftTopRightBottom(l, t, r, b)
        }
    }

    private fun getInsetsListener(): StatusBarContentInsetsChangedListener {
        val callbackCaptor = argumentCaptor<StatusBarContentInsetsChangedListener>()
        verify(insetsProvider).addCallback(capture(callbackCaptor))
        return callbackCaptor.value!!
    }

    companion object {
        private val portraitArea = Rect(10, 0, 990, 100)
        private val landscapeArea = Rect(10, 0, 1990, 80)
        private val fullScreenSb = Rect(10, 0, 990, 2000)

        // 10px insets on both sides
        private const val insets = 10
    }
}