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

Commit b1662cb2 authored by Ahmed Mehfooz's avatar Ahmed Mehfooz
Browse files

Fix crash in SystemEventChipAnimationController

There is a crash when trying to force measure a
view that hasn't been attached to a window yet.
This change fixes this by waiting for the view to
be attached if it has not been attached yet.

Bug: 419537552
Test: Added a test that crashes without the fix.
Flag: com.android.systemui.status_bar_root_modernization

Change-Id: Ib6e3ca8ed8758b84d41254864c5060c411723721
parent 56b18b0b
Loading
Loading
Loading
Loading
+81 −42
Original line number Diff line number Diff line
@@ -20,9 +20,11 @@ import android.content.Context
import android.graphics.Insets
import android.graphics.Rect
import android.testing.TestableLooper
import android.testing.ViewUtils
import android.view.Gravity
import android.view.View
import android.widget.FrameLayout
import androidx.compose.ui.platform.ComposeView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -55,6 +57,7 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() {
    @Mock private lateinit var sbWindowController: StatusBarWindowController
    @Mock private lateinit var insetsProvider: StatusBarContentInsetsProvider

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

@@ -64,7 +67,6 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() {
        // 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,
@@ -108,18 +110,25 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() {

    @Test
    fun prepareChipAnimation_positionsChip() {
        try {
            ViewUtils.attachView(statusbarFake)
            TestableLooper.get(this).processAllMessages()
            controller.prepareChipAnimation(viewCreator)
        val chipRect = controller.chipBounds

        // SB area = 10, 10, 990, 100
        // chip size = 0, 0, 100, 50
            val chipRect = controller.chipBounds
            assertThat(chipRect).isEqualTo(Rect(890, 30, 990, 80))
        } finally {
            ViewUtils.detachView(statusbarFake)
        }
    }

    @Test
    fun prepareChipAnimation_rotation_repositionsChip() {
        controller.prepareChipAnimation(viewCreator)
        try {
            ViewUtils.attachView(statusbarFake)
            TestableLooper.get(this).processAllMessages()

            controller.prepareChipAnimation(viewCreator)
            // Chip has been prepared, and is located at (890, 30, 990, 75)
            // Rotation should put it into its landscape location:
            // SB area = 10, 10, 1990, 80
@@ -131,18 +140,45 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() {

            val chipRect = controller.chipBounds
            assertThat(chipRect).isEqualTo(Rect(1890, 20, 1990, 70))
        } finally {
            ViewUtils.detachView(statusbarFake)
        }
    }

    /** regression test for b/294462223. */
    @Test
    fun prepareChipAnimation_withComposeView_doesNotCrash() {
        val composeViewCreator: ViewCreator = {
            object : BackgroundAnimatableView {
                override val view = ComposeView(mContext)

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

        try {
            ViewUtils.attachView(statusbarFake)
            // Make sure prepareChipAnimation does not crash when it adds the chip to a ComposeView.
            controller.prepareChipAnimation(composeViewCreator)
        } finally {
            ViewUtils.detachView(statusbarFake)
        }
    }

    /** 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
        try {
            ViewUtils.attachView(statusbarFake)
            TestableLooper.get(this).processAllMessages()

            // 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,
@@ -166,6 +202,9 @@ class SystemEventChipAnimationControllerTest : SysuiTestCase() {
            // THEN it still aligns the chip to the content area provided by the insets provider
            val chipRect = controller.chipBounds
            assertThat(chipRect).isEqualTo(Rect(890, 30, 990, 80))
        } finally {
            ViewUtils.detachView(statusbarFake)
        }
    }

    private class TestView(context: Context) : View(context), BackgroundAnimatableView {
+34 −16
Original line number Diff line number Diff line
@@ -124,23 +124,32 @@ constructor(
                    ),
                )
                it.view.alpha = 0f
                // For some reason, the window view's measured width is always 0 here, so use the
                // parent (status bar)
                it.view.measure(
                    View.MeasureSpec.makeMeasureSpec(
                        (animationWindowView.parent as View).width,
                        AT_MOST,
                    ),
                    View.MeasureSpec.makeMeasureSpec(
                        (animationWindowView.parent as View).height,
                        AT_MOST,
                    ),
                )

                // b/294462223: We are not guaranteed to be attached to a window at this point so we
                // need this check to prevent a crash.
                if (it.view.isAttachedToWindow) {
                    measure(it.view)
                    updateChipBounds(
                        it,
                        contentInsetsProvider.getStatusBarContentAreaForCurrentRotation(),
                    )
                } else {
                    it.view.addOnAttachStateChangeListener(
                        object : View.OnAttachStateChangeListener {
                            override fun onViewAttachedToWindow(v: View) {
                                measure(v)
                                updateChipBounds(
                                    it,
                                    contentInsetsProvider
                                        .getStatusBarContentAreaForCurrentRotation(),
                                )
                                v.removeOnAttachStateChangeListener(this)
                            }

                            override fun onViewDetachedFromWindow(v: View) {}
                        }
                    )
                }
            }
    }

@@ -376,6 +385,15 @@ constructor(
        animRect.set(chipBounds)
    }

    private fun measure(v: View) {
        // For some reason, the window view's measured width is always 0 here, so use the parent
        // (status bar)
        v.measure(
            View.MeasureSpec.makeMeasureSpec((animationWindowView.parent as View).width, AT_MOST),
            View.MeasureSpec.makeMeasureSpec((animationWindowView.parent as View).height, AT_MOST),
        )
    }

    private fun layoutParamsDefault(marginEnd: Int): FrameLayout.LayoutParams =
        FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT).also {
            it.gravity = Gravity.END or Gravity.CENTER_VERTICAL