Fix "No Notification" text not gradually showing on LockScreen

The problem and solution can be summarised in 3 aspects:
- EmptyShadeView visibility was not synchronized with the parent container. To fix that, we will update the `EmptyShadeView` when the container is set to visible (see `NotificationStackScrollLayoutController#updateVisibility`).
- EmptyShadeView visibility rule did not consider transitions when deciding to hide itself on keyguard screen. To fix that, we will compare in the current state is different from the outgoing state in addition to the outgoing state been `Keyguard` (see `NotificaitonStackScrollLayoutController#updateShowEmptyShadeView`).
- The notification layout is visible while on keyguard when in split mode (large screen), different from the phone where a transition is executed. To fix that, we will manually animate the `EmptyShadeView` while in lock screen using `ShadeInterpolation.getAlphaContent` to reproduce the nice fade transition we have in phones (see `StackScrollAlgorithm#updateAlphaState`).

Test: manual (tablet)
  - Turn on your device.
  - If you have notifications:
    - Expand your notifications.
    - Tap "Clear All" button.
  - Expand your notifications slowly.
  - Notice "No Notifications" text will gradually fade in with your movement.

Test: manual (phone)
  - Behaviour did not change.

Test: atest ~/tm-qpr-dev/frameworks/base/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt

Fixes: 239903210

Change-Id: I25c0d3fdb484d27eb319e4e22e23facb32c6f6fa
This commit is contained in:
Marcello Galhardo
2022-08-12 17:17:13 +00:00
parent 2c5fcf4428
commit a05b7e337c
9 changed files with 252 additions and 65 deletions

View File

@@ -24,6 +24,8 @@ import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.row.StackScrollerDecorView;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
@@ -73,6 +75,7 @@ public class EmptyShadeView extends StackScrollerDecorView {
}
@Override
@NonNull
public ExpandableViewState createExpandableViewState() {
return new EmptyShadeViewState();
}

View File

@@ -29,6 +29,8 @@ import android.view.accessibility.AccessibilityNodeInfo;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
import androidx.annotation.NonNull;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.policy.SystemBarUtils;
import com.android.systemui.R;
@@ -153,6 +155,7 @@ public class NotificationShelf extends ActivatableNotificationView implements
}
@Override
@NonNull
public ExpandableViewState createExpandableViewState() {
return new ShelfState();
}

View File

@@ -25,8 +25,6 @@ import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -68,6 +66,9 @@ import android.widget.Chronometer;
import android.widget.FrameLayout;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -3247,6 +3248,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
@Override
@NonNull
public ExpandableViewState createExpandableViewState() {
return new NotificationViewState();
}

View File

@@ -29,6 +29,7 @@ import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.systemui.Dumpable;
@@ -66,7 +67,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable {
protected float mContentTransformationAmount;
protected boolean mIsLastChild;
protected int mContentShift;
private final ExpandableViewState mViewState;
@NonNull private final ExpandableViewState mViewState;
private float mContentTranslation;
protected boolean mLastInSection;
protected boolean mFirstInSection;
@@ -610,6 +611,7 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable {
public void setActualHeightAnimating(boolean animating) {}
@NonNull
protected ExpandableViewState createExpandableViewState() {
return new ExpandableViewState();
}
@@ -642,7 +644,12 @@ public abstract class ExpandableView extends FrameLayout implements Dumpable {
return mViewState;
}
@Nullable public ExpandableViewState getViewState() {
/**
* Get the {@link ExpandableViewState} associated with the view.
*
* @return the ExpandableView's view state.
*/
@NonNull public ExpandableViewState getViewState() {
return mViewState;
}

View File

@@ -23,6 +23,8 @@ import android.util.AttributeSet;
import android.util.IndentingPrintWriter;
import android.view.View;
import androidx.annotation.NonNull;
import com.android.systemui.R;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
import com.android.systemui.statusbar.notification.stack.ViewState;
@@ -142,6 +144,7 @@ public class FooterView extends StackScrollerDecorView {
}
@Override
@NonNull
public ExpandableViewState createExpandableViewState() {
return new FooterViewState();
}

View File

@@ -4460,8 +4460,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
private void updateVisibility() {
boolean shouldShow = !mAmbientState.isFullyHidden() || !onKeyguard();
setVisibility(shouldShow ? View.VISIBLE : View.INVISIBLE);
mController.updateVisibility(!mAmbientState.isFullyHidden() || !onKeyguard());
}
@ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
@@ -4526,17 +4525,21 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
void updateEmptyShadeView(boolean visible, boolean notifVisibleInShade) {
void updateEmptyShadeView(boolean visible, boolean areNotificationsHiddenInShade) {
mEmptyShadeView.setVisible(visible, mIsExpanded && mAnimationsEnabled);
int oldTextRes = mEmptyShadeView.getTextResource();
int newTextRes = notifVisibleInShade
int newTextRes = areNotificationsHiddenInShade
? R.string.dnd_suppressing_shade_text : R.string.empty_shade_text;
if (oldTextRes != newTextRes) {
mEmptyShadeView.setText(newTextRes);
}
}
public boolean isEmptyShadeViewVisible() {
return mEmptyShadeView.isVisible();
}
@ShadeViewRefactor(RefactorComponent.SHADE_VIEW)
public void updateFooterView(boolean visible, boolean showDismissView, boolean showHistory) {
if (mFooterView == null) {

View File

@@ -177,7 +177,6 @@ public class NotificationStackScrollLayoutController {
private NotificationStackScrollLayout mView;
private boolean mFadeNotificationsOnDismiss;
private NotificationSwipeHelper mSwipeHelper;
private boolean mShowEmptyShadeView;
@Nullable private Boolean mHistoryEnabled;
private int mBarState;
private HeadsUpAppearanceController mHeadsUpAppearanceController;
@@ -1173,8 +1172,21 @@ public class NotificationStackScrollLayoutController {
}
/**
* Update whether we should show the empty shade view (no notifications in the shade).
* If so, send the update to our view.
* Set the visibility of the view, and propagate it to specific children.
*
* @param visible either the view is visible or not.
*/
public void updateVisibility(boolean visible) {
mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
if (mView.getVisibility() == View.VISIBLE) {
// Synchronize EmptyShadeView visibility with the parent container.
updateShowEmptyShadeView();
}
}
/**
* Update whether we should show the empty shade view ("no notifications" in the shade).
*
* When in split mode, notifications are always visible regardless of the state of the
* QuickSettings panel. That being the case, empty view is always shown if the other conditions
@@ -1182,18 +1194,31 @@ public class NotificationStackScrollLayoutController {
*/
public void updateShowEmptyShadeView() {
Trace.beginSection("NSSLC.updateShowEmptyShadeView");
mShowEmptyShadeView = mStatusBarStateController.getCurrentOrUpcomingState() != KEYGUARD
&& !mView.isQsFullScreen()
&& getVisibleNotificationCount() == 0;
mView.updateEmptyShadeView(
mShowEmptyShadeView,
mZenModeController.areNotificationsHiddenInShade());
final boolean shouldShow = getVisibleNotificationCount() == 0
&& !mView.isQsFullScreen()
// Hide empty shade view when in transition to Keyguard.
// That avoids "No Notifications" to blink when transitioning to AOD.
// For more details, see: b/228790482
&& !isInTransitionToKeyguard();
mView.updateEmptyShadeView(shouldShow, mZenModeController.areNotificationsHiddenInShade());
Trace.endSection();
}
/**
* @return true if {@link StatusBarStateController} is in transition to the KEYGUARD
* and false otherwise.
*/
private boolean isInTransitionToKeyguard() {
final int currentState = mStatusBarStateController.getState();
final int upcomingState = mStatusBarStateController.getCurrentOrUpcomingState();
return (currentState != upcomingState && upcomingState == KEYGUARD);
}
public boolean isShowingEmptyShadeView() {
return mShowEmptyShadeView;
return mView.isEmptyShadeViewVisible();
}
public void setHeadsUpAnimatingAway(boolean headsUpAnimatingAway) {

View File

@@ -120,11 +120,64 @@ public class StackScrollAlgorithm {
updateClipping(algorithmState, ambientState);
updateSpeedBumpState(algorithmState, speedBumpIndex);
updateShelfState(algorithmState, ambientState);
updateAlphaState(algorithmState, ambientState);
getNotificationChildrenStates(algorithmState, ambientState);
}
private void updateAlphaState(StackScrollAlgorithmState algorithmState,
AmbientState ambientState) {
for (ExpandableView view : algorithmState.visibleChildren) {
final ViewState viewState = view.getViewState();
final boolean isHunGoingToShade = ambientState.isShadeExpanded()
&& view == ambientState.getTrackedHeadsUpRow();
if (isHunGoingToShade) {
// Keep 100% opacity for heads up notification going to shade.
viewState.alpha = 1f;
} else if (ambientState.isOnKeyguard()) {
// Adjust alpha for wakeup to lockscreen.
viewState.alpha = 1f - ambientState.getHideAmount();
} else if (ambientState.isExpansionChanging()) {
// Adjust alpha for shade open & close.
float expansion = ambientState.getExpansionFraction();
viewState.alpha = ambientState.isBouncerInTransit()
? BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansion)
: ShadeInterpolation.getContentAlpha(expansion);
}
// For EmptyShadeView if on keyguard, we need to control the alpha to create
// a nice transition when the user is dragging down the notification panel.
if (view instanceof EmptyShadeView && ambientState.isOnKeyguard()) {
final float fractionToShade = ambientState.getFractionToShade();
viewState.alpha = ShadeInterpolation.getContentAlpha(fractionToShade);
}
NotificationShelf shelf = ambientState.getShelf();
if (shelf != null) {
final ViewState shelfState = shelf.getViewState();
// After the shelf has updated its yTranslation, explicitly set alpha=0 for view
// below shelf to skip rendering them in the hardware layer. We do not set them
// invisible because that runs invalidate & onDraw when these views return onscreen,
// which is more expensive.
if (shelfState.hidden) {
// When the shelf is hidden, it won't clip views, so we don't hide rows
return;
}
final float shelfTop = shelfState.yTranslation;
final float viewTop = viewState.yTranslation;
if (viewTop >= shelfTop) {
viewState.alpha = 0;
}
}
}
}
/**
* How expanded or collapsed notifications are when pulling down the shade.
*
* @param ambientState Current ambient state.
* @return 0 when fully collapsed, 1 when expanded.
*/
@@ -208,22 +261,6 @@ public class StackScrollAlgorithm {
}
shelf.updateState(algorithmState, ambientState);
// After the shelf has updated its yTranslation, explicitly set alpha=0 for view below shelf
// to skip rendering them in the hardware layer. We do not set them invisible because that
// runs invalidate & onDraw when these views return onscreen, which is more expensive.
if (shelf.getViewState().hidden) {
// When the shelf is hidden, it won't clip views, so we don't hide rows
return;
}
final float shelfTop = shelf.getViewState().yTranslation;
for (ExpandableView view : algorithmState.visibleChildren) {
final float viewTop = view.getViewState().yTranslation;
if (viewTop >= shelfTop) {
view.getViewState().alpha = 0;
}
}
}
private void updateClipping(StackScrollAlgorithmState algorithmState,
@@ -473,21 +510,6 @@ public class StackScrollAlgorithm {
ExpandableViewState viewState = view.getViewState();
viewState.location = ExpandableViewState.LOCATION_UNKNOWN;
final boolean isHunGoingToShade = ambientState.isShadeExpanded()
&& view == ambientState.getTrackedHeadsUpRow();
if (isHunGoingToShade) {
// Keep 100% opacity for heads up notification going to shade.
} else if (ambientState.isOnKeyguard()) {
// Adjust alpha for wakeup to lockscreen.
viewState.alpha = 1f - ambientState.getHideAmount();
} else if (ambientState.isExpansionChanging()) {
// Adjust alpha for shade open & close.
float expansion = ambientState.getExpansionFraction();
viewState.alpha = ambientState.isBouncerInTransit()
? BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(expansion)
: ShadeInterpolation.getContentAlpha(expansion);
}
final float expansionFraction = getExpansionFractionWithoutShelf(
algorithmState, ambientState);

View File

@@ -3,19 +3,21 @@ package com.android.systemui.statusbar.notification.stack
import android.annotation.DimenRes
import android.widget.FrameLayout
import androidx.test.filters.SmallTest
import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress
import com.android.systemui.R
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.ShadeInterpolation.getContentAlpha
import com.android.systemui.dump.DumpManager
import com.android.systemui.statusbar.EmptyShadeView
import com.android.systemui.statusbar.NotificationShelf
import com.android.systemui.statusbar.StatusBarState
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
import com.android.systemui.statusbar.notification.row.ExpandableView
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.BypassController
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm.SectionProvider
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertEquals
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import junit.framework.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.mock
@@ -26,17 +28,20 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
private val hostView = FrameLayout(context)
private val stackScrollAlgorithm = StackScrollAlgorithm(context, hostView)
private val expandableViewState = ExpandableViewState()
private val notificationRow = mock(ExpandableNotificationRow::class.java)
private val dumpManager = mock(DumpManager::class.java)
private val mStatusBarKeyguardViewManager = mock(StatusBarKeyguardViewManager::class.java)
private val notificationShelf = mock(NotificationShelf::class.java)
private val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply {
layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
}
private val ambientState = AmbientState(
context,
dumpManager,
SectionProvider { _, _ -> false },
BypassController { false },
mStatusBarKeyguardViewManager
context,
dumpManager,
/* sectionProvider */ { _, _ -> false },
/* bypassController */ { false },
mStatusBarKeyguardViewManager
)
private val testableResources = mContext.orCreateTestableResources
@@ -49,7 +54,9 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
@Before
fun setUp() {
whenever(notificationRow.viewState).thenReturn(expandableViewState)
whenever(notificationShelf.viewState).thenReturn(ExpandableViewState())
whenever(notificationRow.viewState).thenReturn(ExpandableViewState())
hostView.addView(notificationRow)
}
@@ -60,7 +67,8 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
stackScrollAlgorithm.resetViewStates(ambientState, 0)
assertThat(expandableViewState.yTranslation).isEqualTo(stackScrollAlgorithm.mHeadsUpInset)
assertThat(notificationRow.viewState.yTranslation)
.isEqualTo(stackScrollAlgorithm.mHeadsUpInset)
}
@Test
@@ -75,15 +83,12 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
stackScrollAlgorithm.resetViewStates(ambientState, 0)
// top margin presence should decrease heads up translation up to minHeadsUpTranslation
assertThat(expandableViewState.yTranslation).isEqualTo(minHeadsUpTranslation)
assertThat(notificationRow.viewState.yTranslation).isEqualTo(minHeadsUpTranslation)
}
@Test
fun resetViewStates_emptyShadeView_isCenteredVertically() {
stackScrollAlgorithm.initView(context)
val emptyShadeView = EmptyShadeView(context, /* attrs= */ null).apply {
layout(/* l= */ 0, /* t= */ 0, /* r= */ 100, /* b= */ 100)
}
hostView.removeAllViews()
hostView.addView(emptyShadeView)
ambientState.layoutMaxHeight = 1280
@@ -97,6 +102,121 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
assertThat(emptyShadeView.viewState?.yTranslation).isEqualTo(centeredY)
}
@Test
fun resetViewStates_hunGoingToShade_viewBecomesOpaque() {
whenever(notificationRow.isAboveShelf).thenReturn(true)
ambientState.isShadeExpanded = true
ambientState.trackedHeadsUpRow = notificationRow
stackScrollAlgorithm.initView(context)
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
assertThat(notificationRow.viewState.alpha).isEqualTo(1f)
}
@Test
fun resetViewStates_isExpansionChanging_viewBecomesTransparent() {
whenever(mStatusBarKeyguardViewManager.isBouncerInTransit).thenReturn(false)
ambientState.isExpansionChanging = true
ambientState.expansionFraction = 0.25f
stackScrollAlgorithm.initView(context)
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
val expected = getContentAlpha(0.25f)
assertThat(notificationRow.viewState.alpha).isEqualTo(expected)
}
@Test
fun resetViewStates_isExpansionChangingWhileBouncerInTransit_viewBecomesTransparent() {
whenever(mStatusBarKeyguardViewManager.isBouncerInTransit).thenReturn(true)
ambientState.isExpansionChanging = true
ambientState.expansionFraction = 0.25f
stackScrollAlgorithm.initView(context)
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
val expected = aboutToShowBouncerProgress(0.25f)
assertThat(notificationRow.viewState.alpha).isEqualTo(expected)
}
@Test
fun resetViewStates_isOnKeyguard_viewBecomesTransparent() {
ambientState.setStatusBarState(StatusBarState.KEYGUARD)
ambientState.hideAmount = 0.25f
stackScrollAlgorithm.initView(context)
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
assertThat(notificationRow.viewState.alpha).isEqualTo(1f - ambientState.hideAmount)
}
@Test
fun resetViewStates_isOnKeyguard_emptyShadeViewBecomesTransparent() {
ambientState.setStatusBarState(StatusBarState.KEYGUARD)
ambientState.fractionToShade = 0.25f
stackScrollAlgorithm.initView(context)
hostView.removeAllViews()
hostView.addView(emptyShadeView)
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
val expected = getContentAlpha(ambientState.fractionToShade)
assertThat(emptyShadeView.viewState.alpha).isEqualTo(expected)
}
@Test
fun resetViewStates_isOnKeyguard_emptyShadeViewBecomesOpaque() {
ambientState.setStatusBarState(StatusBarState.SHADE)
ambientState.fractionToShade = 0.25f
stackScrollAlgorithm.initView(context)
hostView.removeAllViews()
hostView.addView(emptyShadeView)
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
assertThat(emptyShadeView.viewState.alpha).isEqualTo(1f)
}
@Test
fun resetViewStates_hiddenShelf_viewAlphaDoesNotChange() {
val expected = notificationShelf.viewState.alpha
notificationShelf.viewState.hidden = true
ambientState.shelf = notificationShelf
stackScrollAlgorithm.initView(context)
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
assertThat(notificationShelf.viewState.alpha).isEqualTo(expected)
}
@Test
fun resetViewStates_shelfTopLessThanViewTop_hidesView() {
notificationRow.viewState.yTranslation = 10f
notificationShelf.viewState.yTranslation = 0.9f
notificationShelf.viewState.hidden = false
ambientState.shelf = notificationShelf
stackScrollAlgorithm.initView(context)
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
assertThat(notificationRow.viewState.alpha).isEqualTo(0f)
}
@Test
fun resetViewStates_shelfTopGreaterOrEqualThanViewTop_viewAlphaDoesNotChange() {
val expected = notificationRow.viewState.alpha
notificationRow.viewState.yTranslation = 10f
notificationShelf.viewState.yTranslation = 10f
notificationShelf.viewState.hidden = false
ambientState.shelf = notificationShelf
stackScrollAlgorithm.initView(context)
stackScrollAlgorithm.resetViewStates(ambientState, /* speedBumpIndex= */ 0)
assertThat(notificationRow.viewState.alpha).isEqualTo(expected)
}
@Test
fun getGapForLocation_onLockscreen_returnsSmallGap() {
val gap = stackScrollAlgorithm.getGapForLocation(
@@ -267,7 +387,6 @@ class StackScrollAlgorithmTest : SysuiTestCase() {
assertEquals(10f, expandableViewState.yTranslation)
}
@Test
fun clampHunToTop_viewYFarAboveVisibleStack_heightCollapsed() {
val expandableViewState = ExpandableViewState()