diff options
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 + } +} |