From d6a6294fe422bf901ea3551bc0aa985a569f99f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20G=C3=B6llner?= Date: Wed, 10 Aug 2022 15:56:26 +0200 Subject: [PATCH] [Split shade] Fix QS actions footer border visible on shade expansion * Also fixes the actions footer fading in too late on split shade The QS actions footer has its own background and elevation, which makes it become visible during the split shade expansion. The fix is to make sure that there different expansion values are used for the actions footer when on split shade, so that the background of the actions footer fades in later. Change-Id: I81dcba67b9473d9a8a4565dcdffd0a62f3817a50 Test: FooterActionsControllerTest.kt Test: QSFragmentTest.java Test: Manually Fixes: 240563302 --- .../systemui/qs/FooterActionsController.kt | 57 +++++- .../android/systemui/qs/FooterActionsView.kt | 16 +- .../com/android/systemui/qs/QSFragment.java | 8 +- .../qs/FooterActionsControllerTest.kt | 189 ++++++++++++++---- .../android/systemui/qs/QSFragmentTest.java | 34 ++++ 5 files changed, 255 insertions(+), 49 deletions(-) diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt index d701f33c4c666..c790cfe7b7b7c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsController.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs import android.content.Intent +import android.content.res.Configuration import android.os.Handler import android.os.UserManager import android.provider.Settings @@ -38,9 +39,11 @@ import com.android.systemui.qs.dagger.QSFlagsModule.PM_LITE_ENABLED import com.android.systemui.qs.dagger.QSScope import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.MultiUserSwitchController +import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.DeviceProvisionedController import com.android.systemui.statusbar.policy.UserInfoController import com.android.systemui.statusbar.policy.UserInfoController.OnUserInfoChangedListener +import com.android.systemui.util.LargeScreenUtils import com.android.systemui.util.ViewController import com.android.systemui.util.settings.GlobalSettings import javax.inject.Inject @@ -69,18 +72,43 @@ internal class FooterActionsController @Inject constructor( private val uiEventLogger: UiEventLogger, @Named(PM_LITE_ENABLED) private val showPMLiteButton: Boolean, private val globalSetting: GlobalSettings, - private val handler: Handler + private val handler: Handler, + private val configurationController: ConfigurationController, ) : ViewController(view) { private var globalActionsDialog: GlobalActionsDialogLite? = null private var lastExpansion = -1f private var listening: Boolean = false + private var inSplitShade = false - private val alphaAnimator = TouchAnimator.Builder() - .addFloat(mView, "alpha", 0f, 1f) - .setStartDelay(0.9f) + private val singleShadeAnimator by lazy { + // In single shade, the actions footer should only appear at the end of the expansion, + // so that it doesn't overlap with the notifications panel. + TouchAnimator.Builder().addFloat(mView, "alpha", 0f, 1f).setStartDelay(0.9f).build() + } + + private val splitShadeAnimator by lazy { + // The Actions footer view has its own background which is the same color as the qs panel's + // background. + // We don't want it to fade in at the same time as the rest of the panel, otherwise it is + // more opaque than the rest of the panel's background. Only applies to split shade. + val alphaAnimator = TouchAnimator.Builder().addFloat(mView, "alpha", 0f, 1f).build() + val bgAlphaAnimator = + TouchAnimator.Builder() + .addFloat(mView, "backgroundAlpha", 0f, 1f) + .setStartDelay(0.9f) + .build() + // In split shade, we want the actions footer to fade in exactly at the same time as the + // rest of the shade, as there is no overlap. + TouchAnimator.Builder() + .addFloat(alphaAnimator, "position", 0f, 1f) + .addFloat(bgAlphaAnimator, "position", 0f, 1f) .build() + } + + private val animators: TouchAnimator + get() = if (inSplitShade) splitShadeAnimator else singleShadeAnimator var visible = true set(value) { @@ -95,9 +123,7 @@ internal class FooterActionsController @Inject constructor( private val multiUserSwitchController = multiUserSwitchControllerFactory.create(view) @VisibleForTesting - internal val securityFootersSeparator = View(context).apply { - visibility = View.GONE - } + internal val securityFootersSeparator = View(context).apply { visibility = View.GONE } private val onUserInfoChangedListener = OnUserInfoChangedListener { _, picture, _ -> val isGuestUser: Boolean = userManager.isGuestUser(KeyguardUpdateMonitor.getCurrentUser()) @@ -133,6 +159,17 @@ internal class FooterActionsController @Inject constructor( } } + private val configurationListener = + object : ConfigurationController.ConfigurationListener { + override fun onConfigChanged(newConfig: Configuration?) { + updateResources() + } + } + + private fun updateResources() { + inSplitShade = LargeScreenUtils.shouldUseSplitNotificationShade(resources) + } + override fun onInit() { multiUserSwitchController.init() securityFooterController.init() @@ -189,6 +226,9 @@ internal class FooterActionsController @Inject constructor( securityFooterController.setOnVisibilityChangedListener(visibilityListener) fgsManagerFooterController.setOnVisibilityChangedListener(visibilityListener) + configurationController.addCallback(configurationListener) + + updateResources() updateView() } @@ -201,6 +241,7 @@ internal class FooterActionsController @Inject constructor( globalActionsDialog = null setListening(false) multiUserSetting.isListening = false + configurationController.removeCallback(configurationListener) } fun setListening(listening: Boolean) { @@ -224,7 +265,7 @@ internal class FooterActionsController @Inject constructor( } fun setExpansion(headerExpansionFraction: Float) { - alphaAnimator.setPosition(headerExpansionFraction) + animators.setPosition(headerExpansionFraction) } fun setKeyguardShowing(showing: Boolean) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt index 05038b7b1b1d7..309ac2a66e6b9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/FooterActionsView.kt @@ -27,6 +27,7 @@ import android.view.MotionEvent import android.view.View import android.widget.ImageView import android.widget.LinearLayout +import androidx.annotation.Keep import com.android.settingslib.Utils import com.android.settingslib.drawable.UserIconDrawable import com.android.systemui.R @@ -45,6 +46,19 @@ class FooterActionsView(context: Context?, attrs: AttributeSet?) : LinearLayout( private var qsDisabled = false private var expansionAmount = 0f + /** + * Sets the alpha of the background of this view. + * + * Used from a [TouchAnimator] in the controller. + */ + var backgroundAlpha: Float = 1f + @Keep + set(value) { + field = value + background?.alpha = (value * 255).toInt() + } + @Keep get + override fun onFinishInflate() { super.onFinishInflate() settingsContainer = findViewById(R.id.settings_button_container) @@ -117,4 +131,4 @@ class FooterActionsView(context: Context?, attrs: AttributeSet?) : LinearLayout( private const val TAG = "FooterActionsView" private val VERBOSE = Log.isLoggable(TAG, Log.VERBOSE) private val MotionEvent.string - get() = "($id): ($x,$y)" \ No newline at end of file + get() = "($id): ($x,$y)" diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java index 8c5e6cc631471..139fb8b0bc14e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSFragment.java @@ -557,9 +557,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca public void setQsExpansion(float expansion, float panelExpansionFraction, float proposedTranslation, float squishinessFraction) { float headerTranslation = mTransitioningToFullShade ? 0 : proposedTranslation; - float progress = mTransitioningToFullShade || mState == StatusBarState.KEYGUARD + float alphaProgress = mTransitioningToFullShade || mState == StatusBarState.KEYGUARD ? mFullShadeProgress : panelExpansionFraction; - setAlphaAnimationProgress(mInSplitShade ? progress : 1); + setAlphaAnimationProgress(mInSplitShade ? alphaProgress : 1); mContainer.setExpansion(expansion); final float translationScaleY = (mInSplitShade ? 1 : QSAnimator.SHORT_PARALLAX_AMOUNT) * (expansion - 1); @@ -600,7 +600,9 @@ public class QSFragment extends LifecycleFragment implements QS, CommandQueue.Ca } mQSPanelController.setIsOnKeyguard(onKeyguard); mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion); - mQSFooterActionController.setExpansion(onKeyguardAndExpanded ? 1 : expansion); + float footerActionsExpansion = + onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion; + mQSFooterActionController.setExpansion(footerActionsExpansion); mQSPanelController.setRevealExpansion(expansion); mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt index 642e29b364d34..2ba8782c6d02c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/FooterActionsControllerTest.kt @@ -22,13 +22,17 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.phone.MultiUserSwitchController import com.android.systemui.statusbar.policy.DeviceProvisionedController +import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.UserInfoController import com.android.systemui.util.mockito.capture import com.android.systemui.util.settings.FakeSettings import com.android.systemui.utils.leaks.LeakCheckedTest +import com.google.common.truth.Expect import com.google.common.truth.Truth.assertThat +import javax.inject.Provider import org.junit.After import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -42,47 +46,38 @@ import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import javax.inject.Provider import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) @RunWith(AndroidTestingRunner::class) class FooterActionsControllerTest : LeakCheckedTest() { - @Mock - private lateinit var userManager: UserManager - @Mock - private lateinit var userTracker: UserTracker - @Mock - private lateinit var activityStarter: ActivityStarter - @Mock - private lateinit var deviceProvisionedController: DeviceProvisionedController - @Mock - private lateinit var userInfoController: UserInfoController - @Mock - private lateinit var multiUserSwitchControllerFactory: MultiUserSwitchController.Factory - @Mock - private lateinit var multiUserSwitchController: MultiUserSwitchController - @Mock - private lateinit var globalActionsDialogProvider: Provider - @Mock - private lateinit var globalActionsDialog: GlobalActionsDialogLite - @Mock - private lateinit var uiEventLogger: UiEventLogger - @Mock - private lateinit var securityFooterController: QSSecurityFooter - @Mock - private lateinit var fgsManagerController: QSFgsManagerFooter + + @get:Rule var expect: Expect = Expect.create() + + @Mock private lateinit var userManager: UserManager + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var activityStarter: ActivityStarter + @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController + @Mock private lateinit var userInfoController: UserInfoController + @Mock private lateinit var multiUserSwitchControllerFactory: MultiUserSwitchController.Factory + @Mock private lateinit var multiUserSwitchController: MultiUserSwitchController + @Mock private lateinit var globalActionsDialogProvider: Provider + @Mock private lateinit var globalActionsDialog: GlobalActionsDialogLite + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var securityFooterController: QSSecurityFooter + @Mock private lateinit var fgsManagerController: QSFgsManagerFooter @Captor private lateinit var visibilityChangedCaptor: ArgumentCaptor private lateinit var controller: FooterActionsController + private val configurationController = FakeConfigurationController() private val metricsLogger: MetricsLogger = FakeMetricsLogger() - private lateinit var view: FooterActionsView private val falsingManager: FalsingManagerFake = FalsingManagerFake() + private lateinit var view: FooterActionsView private lateinit var testableLooper: TestableLooper private lateinit var fakeSettings: FakeSettings private lateinit var securityFooter: View @@ -90,12 +85,15 @@ class FooterActionsControllerTest : LeakCheckedTest() { @Before fun setUp() { + // We want to make sure testable resources are always used + context.ensureTestableResources() + MockitoAnnotations.initMocks(this) testableLooper = TestableLooper.get(this) fakeSettings = FakeSettings() whenever(multiUserSwitchControllerFactory.create(any())) - .thenReturn(multiUserSwitchController) + .thenReturn(multiUserSwitchController) whenever(globalActionsDialogProvider.get()).thenReturn(globalActionsDialog) securityFooter = View(mContext) @@ -135,7 +133,7 @@ class FooterActionsControllerTest : LeakCheckedTest() { view.findViewById(R.id.pm_lite).performClick() // Verify clicks are logged verify(uiEventLogger, Mockito.times(1)) - .log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) + .log(GlobalActionsDialogLite.GlobalActionsEvent.GA_OPEN_QS) } @Test @@ -299,6 +297,86 @@ class FooterActionsControllerTest : LeakCheckedTest() { assertThat(booleanCaptor.allValues.last()).isTrue() } + @Test + fun setExpansion_inSplitShade_alphaFollowsExpansion() { + enableSplitShade() + + controller.setExpansion(0f) + expect.that(view.alpha).isEqualTo(0f) + + controller.setExpansion(0.25f) + expect.that(view.alpha).isEqualTo(0.25f) + + controller.setExpansion(0.5f) + expect.that(view.alpha).isEqualTo(0.5f) + + controller.setExpansion(0.75f) + expect.that(view.alpha).isEqualTo(0.75f) + + controller.setExpansion(1f) + expect.that(view.alpha).isEqualTo(1f) + } + + @Test + fun setExpansion_inSplitShade_backgroundAlphaFollowsExpansion_with_0_9_delay() { + enableSplitShade() + + controller.setExpansion(0f) + expect.that(view.backgroundAlphaFraction).isEqualTo(0f) + + controller.setExpansion(0.5f) + expect.that(view.backgroundAlphaFraction).isEqualTo(0f) + + controller.setExpansion(0.9f) + expect.that(view.backgroundAlphaFraction).isEqualTo(0f) + + controller.setExpansion(0.91f) + expect.that(view.backgroundAlphaFraction).isWithin(FLOAT_TOLERANCE).of(0.1f) + + controller.setExpansion(0.95f) + expect.that(view.backgroundAlphaFraction).isWithin(FLOAT_TOLERANCE).of(0.5f) + + controller.setExpansion(1f) + expect.that(view.backgroundAlphaFraction).isEqualTo(1f) + } + + @Test + fun setExpansion_inSingleShade_alphaFollowsExpansion_with_0_9_delay() { + disableSplitShade() + + controller.setExpansion(0f) + expect.that(view.alpha).isEqualTo(0f) + + controller.setExpansion(0.5f) + expect.that(view.alpha).isEqualTo(0f) + + controller.setExpansion(0.9f) + expect.that(view.alpha).isEqualTo(0f) + + controller.setExpansion(0.91f) + expect.that(view.alpha).isWithin(FLOAT_TOLERANCE).of(0.1f) + + controller.setExpansion(0.95f) + expect.that(view.alpha).isWithin(FLOAT_TOLERANCE).of(0.5f) + + controller.setExpansion(1f) + expect.that(view.alpha).isEqualTo(1f) + } + + @Test + fun setExpansion_inSingleShade_backgroundAlphaAlways1() { + disableSplitShade() + + controller.setExpansion(0f) + expect.that(view.backgroundAlphaFraction).isEqualTo(1f) + + controller.setExpansion(0.5f) + expect.that(view.backgroundAlphaFraction).isEqualTo(1f) + + controller.setExpansion(1f) + expect.that(view.backgroundAlphaFraction).isEqualTo(1f) + } + private fun setVisibilities( securityFooterVisible: Boolean, fgsFooterVisible: Boolean, @@ -311,15 +389,52 @@ class FooterActionsControllerTest : LeakCheckedTest() { } private fun inflateView(): FooterActionsView { - return LayoutInflater.from(context) - .inflate(R.layout.footer_actions, null) as FooterActionsView + return LayoutInflater.from(context).inflate(R.layout.footer_actions, null) + as FooterActionsView } private fun constructFooterActionsController(view: FooterActionsView): FooterActionsController { - return FooterActionsController(view, multiUserSwitchControllerFactory, - activityStarter, userManager, userTracker, userInfoController, - deviceProvisionedController, securityFooterController, fgsManagerController, - falsingManager, metricsLogger, globalActionsDialogProvider, uiEventLogger, - showPMLiteButton = true, fakeSettings, Handler(testableLooper.looper)) + return FooterActionsController( + view, + multiUserSwitchControllerFactory, + activityStarter, + userManager, + userTracker, + userInfoController, + deviceProvisionedController, + securityFooterController, + fgsManagerController, + falsingManager, + metricsLogger, + globalActionsDialogProvider, + uiEventLogger, + showPMLiteButton = true, + fakeSettings, + Handler(testableLooper.looper), + configurationController) + } + + private fun enableSplitShade() { + setSplitShadeEnabled(true) + } + + private fun disableSplitShade() { + setSplitShadeEnabled(false) + } + + private fun setSplitShadeEnabled(enabled: Boolean) { + overrideResource(R.bool.config_use_split_notification_shade, enabled) + configurationController.notifyConfigurationChanged() + } +} + +private const val FLOAT_TOLERANCE = 0.01f + +private val View.backgroundAlphaFraction: Float? + get() { + return if (background != null) { + background.alpha / 255f + } else { + null + } } -} \ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java index 32c66d25d4aa6..10f6ce8c0ec9d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java @@ -204,6 +204,40 @@ public class QSFragmentTest extends SysuiBaseFragmentTest { assertThat(mQsFragmentView.getAlpha()).isEqualTo(1); } + @Test + public void setQsExpansion_inSplitShade_setsFooterActionsExpansion_basedOnPanelExpFraction() { + // Random test values without any meaning. They just have to be different from each other. + float expansion = 0.123f; + float panelExpansionFraction = 0.321f; + float proposedTranslation = 456f; + float squishinessFraction = 0.987f; + + QSFragment fragment = resumeAndGetFragment(); + enableSplitShade(); + + fragment.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation, + squishinessFraction); + + verify(mQSFooterActionController).setExpansion(panelExpansionFraction); + } + + @Test + public void setQsExpansion_notInSplitShade_setsFooterActionsExpansion_basedOnExpansion() { + // Random test values without any meaning. They just have to be different from each other. + float expansion = 0.123f; + float panelExpansionFraction = 0.321f; + float proposedTranslation = 456f; + float squishinessFraction = 0.987f; + + QSFragment fragment = resumeAndGetFragment(); + disableSplitShade(); + + fragment.setQsExpansion(expansion, panelExpansionFraction, proposedTranslation, + squishinessFraction); + + verify(mQSFooterActionController).setExpansion(expansion); + } + @Test public void getQsMinExpansionHeight_notInSplitShade_returnsHeaderHeight() { QSFragment fragment = resumeAndGetFragment();