diff options
| author | 2023-06-30 17:43:38 -0400 | |
|---|---|---|
| committer | 2023-07-05 11:43:26 -0400 | |
| commit | 62a8d73e6c0b9875cda41a6062b62ff80d223172 (patch) | |
| tree | f4780eeb34121278836b63b1f867f84256724486 | |
| parent | b1aa946049df2787f4d47244c3298016129d45b5 (diff) | |
[Sb chip] Fix chip layout when fullscreen or rotating
When launching an activity from the ongoing call chip, we end up setting
the StatusBar window to fullscreen for a few frames. This would cause
SystemEventChipAnimationController to position the chip halfway down the
display due to positioning the container view using CENTER_VERTICAL.
While testing, I also discovered that any rotation that occurred during
the animation would not be properly reflected. Opening the camera app
while in landscape would sometimes not show the chip at all due to
triggering a rotation AND a privacy event at the same time. The fix for
this is to use the StatusBarContentInsetsChangedListener to re-solve for
the animation rect bounds.
Test: manual
Test: SystemEventChipAnimationControllerTest
Fixes: 289378932
Change-Id: I9f9909842999dd3c2cbd11c392223b7f213eff3a
2 files changed, 250 insertions, 23 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt index 776956a20140..56390002490c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/SystemEventChipAnimationController.kt @@ -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 = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt new file mode 100644 index 000000000000..55b6be9679f2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/events/SystemEventChipAnimationControllerTest.kt @@ -0,0 +1,189 @@ +/* + * 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 + } +} |