Fix privacy chip overlapping with statusbar icons

According to my investigation there was a race condition on setting the alpha value of the end-side statusbar content. It could happen that the "show animation" for the end-side statusbar content was triggered. If at roughly the same time a statusbar chip animation is started, it could happen, that the chip animation is finished before the "show animation", which causes the "show animation" to set the final alpha value, even though the chip animation should take precedence (since the statusbar icons should always be hidden if there is a chip currently being animated).

Bug: 287923736
Test: atest MultiSourceMinAlphaControllerTest, atest CollapsedStatusBarFragmentTest
Change-Id: Ifa16bf3aff784c98063b09064571a30a51030bb0
This commit is contained in:
Johannes Gallmann
2023-06-20 14:41:25 +02:00
parent 91a8a689fd
commit 01572146e0
4 changed files with 262 additions and 61 deletions

View File

@@ -14,9 +14,6 @@
package com.android.systemui.statusbar.phone.fragment;
import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE;
import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.SHOWING_PERSISTENT_DOT;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.app.Fragment;
@@ -39,6 +36,7 @@ import androidx.annotation.VisibleForTesting;
import androidx.core.animation.Animator;
import com.android.app.animation.Interpolators;
import com.android.app.animation.InterpolatorsAndroidX;
import com.android.keyguard.KeyguardUpdateMonitor;
import com.android.systemui.Dumpable;
import com.android.systemui.R;
@@ -77,6 +75,8 @@ import com.android.systemui.util.CarrierConfigTracker.CarrierConfigChangedListen
import com.android.systemui.util.CarrierConfigTracker.DefaultDataSubscriptionChangedListener;
import com.android.systemui.util.settings.SecureSettings;
import kotlin.Unit;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -99,12 +99,16 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
private static final String EXTRA_PANEL_STATE = "panel_state";
public static final String STATUS_BAR_ICON_MANAGER_TAG = "status_bar_icon_manager";
public static final int FADE_IN_DURATION = 320;
public static final int FADE_OUT_DURATION = 160;
public static final int FADE_IN_DELAY = 50;
private static final int SOURCE_SYSTEM_EVENT_ANIMATOR = 1;
private static final int SOURCE_OTHER = 2;
private StatusBarFragmentComponent mStatusBarFragmentComponent;
private PhoneStatusBarView mStatusBar;
private final StatusBarStateController mStatusBarStateController;
private final KeyguardStateController mKeyguardStateController;
private final ShadeViewController mShadeViewController;
private MultiSourceMinAlphaController mEndSideAlphaController;
private LinearLayout mEndSideContent;
private View mClockView;
private View mOngoingCallChip;
@@ -149,7 +153,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
}
};
private OperatorNameViewController mOperatorNameViewController;
private StatusBarSystemEventAnimator mSystemEventAnimator;
private StatusBarSystemEventDefaultAnimator mSystemEventAnimator;
private final CarrierConfigChangedListener mCarrierConfigCallback =
new CarrierConfigChangedListener() {
@@ -297,14 +301,14 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
updateBlockedIcons();
mStatusBarIconController.addIconGroup(mDarkIconManager);
mEndSideContent = mStatusBar.findViewById(R.id.status_bar_end_side_content);
mEndSideAlphaController = new MultiSourceMinAlphaController(mEndSideContent);
mClockView = mStatusBar.findViewById(R.id.clock);
mOngoingCallChip = mStatusBar.findViewById(R.id.ongoing_call_chip);
showEndSideContent(false);
showClock(false);
initOperatorName();
initNotificationIconArea();
mSystemEventAnimator =
new StatusBarSystemEventAnimator(mEndSideContent, getResources());
mSystemEventAnimator = getSystemEventAnimator();
mCarrierConfigTracker.addCallback(mCarrierConfigCallback);
mCarrierConfigTracker.addDefaultDataSubscriptionChangedListener(mDefaultDataListener);
@@ -593,18 +597,27 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
}
private void hideEndSideContent(boolean animate) {
animateHide(mEndSideContent, animate);
if (!animate) {
mEndSideAlphaController.setAlpha(/*alpha*/ 0f, SOURCE_OTHER);
} else {
mEndSideAlphaController.animateToAlpha(/*alpha*/ 0f, SOURCE_OTHER, FADE_OUT_DURATION,
InterpolatorsAndroidX.ALPHA_OUT, /*startDelay*/ 0);
}
}
private void showEndSideContent(boolean animate) {
// Only show the system icon area if we are not currently animating
int state = mAnimationScheduler.getAnimationState();
if (state == IDLE || state == SHOWING_PERSISTENT_DOT) {
animateShow(mEndSideContent, animate);
if (!animate) {
mEndSideAlphaController.setAlpha(1f, SOURCE_OTHER);
return;
}
if (mKeyguardStateController.isKeyguardFadingAway()) {
mEndSideAlphaController.animateToAlpha(/*alpha*/ 1f, SOURCE_OTHER,
mKeyguardStateController.getKeyguardFadingAwayDuration(),
InterpolatorsAndroidX.LINEAR_OUT_SLOW_IN,
mKeyguardStateController.getKeyguardFadingAwayDelay());
} else {
// We are in the middle of a system status event animation, which will animate the
// alpha (but not the visibility). Allow the view to become visible again
mEndSideContent.setVisibility(View.VISIBLE);
mEndSideAlphaController.animateToAlpha(/*alpha*/ 1f, SOURCE_OTHER, FADE_IN_DURATION,
InterpolatorsAndroidX.ALPHA_IN, FADE_IN_DELAY);
}
}
@@ -671,7 +684,7 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
v.animate()
.alpha(0f)
.setDuration(160)
.setDuration(FADE_OUT_DURATION)
.setStartDelay(0)
.setInterpolator(Interpolators.ALPHA_OUT)
.withEndAction(() -> v.setVisibility(state));
@@ -754,6 +767,16 @@ public class CollapsedStatusBarFragment extends Fragment implements CommandQueue
return mSystemEventAnimator.onSystemEventAnimationFinish(hasPersistentDot);
}
private StatusBarSystemEventDefaultAnimator getSystemEventAnimator() {
return new StatusBarSystemEventDefaultAnimator(getResources(), (alpha) -> {
mEndSideAlphaController.setAlpha(alpha, SOURCE_SYSTEM_EVENT_ANIMATOR);
return Unit.INSTANCE;
}, (translationX) -> {
mEndSideContent.setTranslationX(translationX);
return Unit.INSTANCE;
}, /*isAnimationRunning*/ false);
}
private void updateStatusBarLocation(int left, int right) {
int leftMargin = left - mStatusBar.getLeft();
int rightMargin = mStatusBar.getRight() - right;

View File

@@ -0,0 +1,82 @@
/*
* 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.phone.fragment
import android.view.View
import androidx.core.animation.Interpolator
import androidx.core.animation.ValueAnimator
import com.android.app.animation.InterpolatorsAndroidX
/**
* A controller that keeps track of multiple sources applying alpha value changes to a view. It will
* always apply the minimum alpha value of all sources.
*/
internal class MultiSourceMinAlphaController
@JvmOverloads
constructor(private val view: View, private val initialAlpha: Float = 1f) {
private val alphas = mutableMapOf<Int, Float>()
private val animators = mutableMapOf<Int, ValueAnimator>()
/**
* Sets the alpha of the provided source and applies it to the view (if no other source has set
* a lower alpha currently). If an animator of the same source is still running (i.e.
* [animateToAlpha] was called before), that animator is cancelled.
*/
fun setAlpha(alpha: Float, sourceId: Int) {
animators[sourceId]?.cancel()
updateAlpha(alpha, sourceId)
}
/** Animates to the alpha of the provided source. */
fun animateToAlpha(
alpha: Float,
sourceId: Int,
duration: Long,
interpolator: Interpolator = InterpolatorsAndroidX.ALPHA_IN,
startDelay: Long = 0
) {
animators[sourceId]?.cancel()
val animator = ValueAnimator.ofFloat(getMinAlpha(), alpha)
animator.duration = duration
animator.startDelay = startDelay
animator.interpolator = interpolator
animator.addUpdateListener { updateAlpha(animator.animatedValue as Float, sourceId) }
animator.start()
animators[sourceId] = animator
}
fun reset() {
alphas.clear()
animators.forEach { it.value.cancel() }
animators.clear()
applyAlphaToView()
}
private fun updateAlpha(alpha: Float, sourceId: Int) {
alphas[sourceId] = alpha
applyAlphaToView()
}
private fun applyAlphaToView() {
val minAlpha = getMinAlpha()
view.visibility = if (minAlpha != 0f) View.VISIBLE else View.INVISIBLE
view.alpha = minAlpha
}
private fun getMinAlpha() = alphas.minOfOrNull { it.value } ?: initialAlpha
}

View File

@@ -18,10 +18,6 @@ import static android.view.Display.DEFAULT_DISPLAY;
import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_CLOSED;
import static com.android.systemui.shade.ShadeExpansionStateManagerKt.STATE_OPEN;
import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_IN;
import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.ANIMATING_OUT;
import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.IDLE;
import static com.android.systemui.statusbar.events.SystemStatusAnimationSchedulerKt.RUNNING_CHIP_ANIM;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -49,7 +45,7 @@ import android.view.View;
import android.view.ViewPropertyAnimator;
import android.widget.FrameLayout;
import androidx.core.animation.Animator;
import androidx.core.animation.AnimatorTestRule;
import androidx.test.filters.SmallTest;
import com.android.keyguard.KeyguardUpdateMonitor;
@@ -85,6 +81,7 @@ import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.time.FakeSystemClock;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -139,6 +136,8 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
private StatusBarWindowStateController mStatusBarWindowStateController;
@Mock
private KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@ClassRule
public static AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule();
private List<StatusBarWindowStateListener> mStatusBarWindowStateListeners = new ArrayList<>();
@@ -172,7 +171,6 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
@Test
public void testDisableSystemInfo_systemAnimationIdle_doesHide() {
when(mAnimationScheduler.getAnimationState()).thenReturn(IDLE);
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_SYSTEM_INFO, 0, false);
@@ -192,24 +190,26 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
public void testSystemStatusAnimation_startedDisabled_finishedWithAnimator_showsSystemInfo() {
// GIVEN the status bar hides the system info via disable flags, while there is no event
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
when(mAnimationScheduler.getAnimationState()).thenReturn(IDLE);
fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_SYSTEM_INFO, 0, false);
assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
// WHEN the system event animation starts
fragment.onSystemEventAnimationBegin().start();
// THEN the view remains invisible during the animation
assertEquals(0f, getEndSideContentView().getAlpha(), 0.01);
mAnimatorTestRule.advanceTimeBy(500);
assertEquals(0f, getEndSideContentView().getAlpha(), 0.01);
// WHEN the disable flags are cleared during a system event animation
when(mAnimationScheduler.getAnimationState()).thenReturn(RUNNING_CHIP_ANIM);
fragment.disable(DEFAULT_DISPLAY, 0, 0, false);
// THEN the view is made visible again, but still low alpha
assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
// THEN the view remains invisible
assertEquals(0, getEndSideContentView().getAlpha(), 0.01);
// WHEN the system event animation finishes
when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_OUT);
Animator anim = fragment.onSystemEventAnimationFinish(false);
anim.start();
processAllMessages();
anim.end();
fragment.onSystemEventAnimationFinish(false).start();
mAnimatorTestRule.advanceTimeBy(500);
// THEN the system info is full alpha
assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
@@ -219,20 +219,15 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
public void testSystemStatusAnimation_systemInfoDisabled_staysInvisible() {
// GIVEN the status bar hides the system info via disable flags, while there is no event
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
when(mAnimationScheduler.getAnimationState()).thenReturn(IDLE);
fragment.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_SYSTEM_INFO, 0, false);
assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
// WHEN the system event animation finishes
when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_OUT);
Animator anim = fragment.onSystemEventAnimationFinish(false);
anim.start();
processAllMessages();
anim.end();
fragment.onSystemEventAnimationFinish(false).start();
mAnimatorTestRule.advanceTimeBy(500);
// THEN the system info is at full alpha, but still INVISIBLE (since the disable flag is
// still set)
assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
// THEN the system info remains invisible (since the disable flag is still set)
assertEquals(0, getEndSideContentView().getAlpha(), 0.01);
assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
}
@@ -241,15 +236,14 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
public void testSystemStatusAnimation_notDisabled_animatesAlphaZero() {
// GIVEN the status bar is not disabled
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_IN);
// WHEN the system event animation begins
Animator anim = fragment.onSystemEventAnimationBegin();
anim.start();
processAllMessages();
anim.end();
assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
// THEN the system info is visible but alpha 0
assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
// WHEN the system event animation begins
fragment.onSystemEventAnimationBegin().start();
mAnimatorTestRule.advanceTimeBy(500);
// THEN the system info is invisible
assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
assertEquals(0, getEndSideContentView().getAlpha(), 0.01);
}
@@ -257,25 +251,21 @@ public class CollapsedStatusBarFragmentTest extends SysuiBaseFragmentTest {
public void testSystemStatusAnimation_notDisabled_animatesBackToAlphaOne() {
// GIVEN the status bar is not disabled
CollapsedStatusBarFragment fragment = resumeAndGetFragment();
when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_IN);
// WHEN the system event animation begins
Animator anim = fragment.onSystemEventAnimationBegin();
anim.start();
processAllMessages();
anim.end();
assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
// THEN the system info is visible but alpha 0
assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
// WHEN the system event animation begins
fragment.onSystemEventAnimationBegin().start();
mAnimatorTestRule.advanceTimeBy(500);
// THEN the system info is invisible
assertEquals(View.INVISIBLE, getEndSideContentView().getVisibility());
assertEquals(0, getEndSideContentView().getAlpha(), 0.01);
// WHEN the system event animation finishes
when(mAnimationScheduler.getAnimationState()).thenReturn(ANIMATING_OUT);
anim = fragment.onSystemEventAnimationFinish(false);
anim.start();
processAllMessages();
anim.end();
fragment.onSystemEventAnimationFinish(false).start();
mAnimatorTestRule.advanceTimeBy(500);
// THEN the syste info is full alpha and VISIBLE
// THEN the system info is full alpha and VISIBLE
assertEquals(View.VISIBLE, getEndSideContentView().getVisibility());
assertEquals(1, getEndSideContentView().getAlpha(), 0.01);
}

View File

@@ -0,0 +1,106 @@
/*
* 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.phone.fragment
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import android.view.View
import androidx.core.animation.AnimatorTestRule
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
private const val TEST_SOURCE_1 = 1
private const val TEST_SOURCE_2 = 2
private const val TEST_ANIMATION_DURATION = 100L
private const val INITIAL_ALPHA = 1f
@RunWith(AndroidTestingRunner::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@SmallTest
class MultiSourceMinAlphaControllerTest : SysuiTestCase() {
private val view = View(context)
private val multiSourceMinAlphaController =
MultiSourceMinAlphaController(view, initialAlpha = INITIAL_ALPHA)
@get:Rule val animatorTestRule = AnimatorTestRule()
@Before
fun setup() {
multiSourceMinAlphaController.reset()
}
@Test
fun testSetAlpha() {
multiSourceMinAlphaController.setAlpha(alpha = 0.5f, sourceId = TEST_SOURCE_1)
assertEquals(0.5f, view.alpha)
}
@Test
fun testAnimateToAlpha() {
multiSourceMinAlphaController.animateToAlpha(
alpha = 0.5f,
sourceId = TEST_SOURCE_1,
duration = TEST_ANIMATION_DURATION
)
animatorTestRule.advanceTimeBy(TEST_ANIMATION_DURATION)
assertEquals(0.5f, view.alpha)
}
@Test
fun testReset() {
multiSourceMinAlphaController.animateToAlpha(
alpha = 0.5f,
sourceId = TEST_SOURCE_1,
duration = TEST_ANIMATION_DURATION
)
multiSourceMinAlphaController.setAlpha(alpha = 0.7f, sourceId = TEST_SOURCE_2)
multiSourceMinAlphaController.reset()
// advance time to ensure that animators are cancelled when the controller is reset
animatorTestRule.advanceTimeBy(TEST_ANIMATION_DURATION)
assertEquals(INITIAL_ALPHA, view.alpha)
}
@Test
fun testMinOfTwoSourcesIsApplied() {
multiSourceMinAlphaController.setAlpha(alpha = 0f, sourceId = TEST_SOURCE_1)
multiSourceMinAlphaController.setAlpha(alpha = 0.5f, sourceId = TEST_SOURCE_2)
assertEquals(0f, view.alpha)
multiSourceMinAlphaController.setAlpha(alpha = 1f, sourceId = TEST_SOURCE_1)
assertEquals(0.5f, view.alpha)
}
@Test
fun testSetAlphaForSameSourceCancelsAnimator() {
multiSourceMinAlphaController.animateToAlpha(
alpha = 0f,
sourceId = TEST_SOURCE_1,
duration = TEST_ANIMATION_DURATION
)
animatorTestRule.advanceTimeBy(TEST_ANIMATION_DURATION / 2)
multiSourceMinAlphaController.setAlpha(alpha = 1f, sourceId = TEST_SOURCE_1)
animatorTestRule.advanceTimeBy(TEST_ANIMATION_DURATION / 2)
// verify that animation was cancelled and the setAlpha call overrides the alpha value of
// the animation
assertEquals(1f, view.alpha)
}
}