diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java index afce945a5bc54..734bc48093b2c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShelf.java @@ -39,7 +39,6 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.row.ActivatableNotificationView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; -import com.android.systemui.statusbar.notification.row.NotificationBackgroundView; import com.android.systemui.statusbar.notification.stack.AmbientState; import com.android.systemui.statusbar.notification.stack.AnimationProperties; import com.android.systemui.statusbar.notification.stack.ExpandableViewState; @@ -411,7 +410,7 @@ public class NotificationShelf extends ActivatableNotificationView implements setBackgroundTop(backgroundTop); setFirstElementRoundness(firstElementRoundness); mShelfIcons.setSpeedBumpIndex(mHostLayoutController.getSpeedBumpIndex()); - mShelfIcons.calculateIconTranslations(); + mShelfIcons.calculateIconXTranslations(); mShelfIcons.applyIconStates(); for (int i = 0; i < mHostLayoutController.getChildCount(); i++) { View child = mHostLayoutController.getChildAt(i); @@ -636,7 +635,7 @@ public class NotificationShelf extends ActivatableNotificationView implements float viewEnd = viewStart + fullHeight; float fullTransitionAmount = 0.0f; float iconTransitionAmount = 0.0f; - float shelfStart = getTranslationY(); + float shelfStart = getTranslationY() - mPaddingBetweenElements; if (mAmbientState.isExpansionChanging() && !mAmbientState.isOnKeyguard()) { // TODO(b/172289889) handle icon placement for notification that is clipped by the shelf if (mIndexOfFirstViewInShelf != -1 && i >= mIndexOfFirstViewInShelf) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt index 2b11f693da1d5..9ea36d540f4d7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculator.kt @@ -207,9 +207,7 @@ constructor( visibleIndex: Int ): Float { var height = stack.calculateGapHeight(previous, current, visibleIndex) - if (visibleIndex != 0) { - height += dividerHeight - } + height += dividerHeight return height } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java index 22242b8fb4b4f..2b79986662f18 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithm.java @@ -314,7 +314,8 @@ public class StackScrollAlgorithm { if (ambientState.getShelf() != null) { final float shelfStart = ambientState.getStackEndHeight() - - ambientState.getShelf().getIntrinsicHeight(); + - ambientState.getShelf().getIntrinsicHeight() + - mPaddingBetweenElements; if (currentY >= shelfStart && !(view instanceof FooterView) && state.firstViewInShelf == null) { @@ -507,8 +508,9 @@ public class StackScrollAlgorithm { || bypassPulseNotExpanding ? ambientState.getInnerHeight() : (int) ambientState.getStackHeight(); - final int shelfStart = - stackBottom - ambientState.getShelf().getIntrinsicHeight(); + final int shelfStart = stackBottom + - ambientState.getShelf().getIntrinsicHeight() + - mPaddingBetweenElements; viewState.yTranslation = Math.min(viewState.yTranslation, shelfStart); if (viewState.yTranslation >= shelfStart) { viewState.hidden = !view.isExpandAnimationRunning() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java index 034b751d1e61d..2dc3261eb8861 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationIconContainer.java @@ -27,11 +27,13 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.drawable.Icon; import android.util.AttributeSet; +import android.util.MathUtils; import android.util.Property; import android.view.ContextThemeWrapper; import android.view.View; import android.view.animation.Interpolator; +import androidx.annotation.VisibleForTesting; import androidx.collection.ArrayMap; import com.android.internal.statusbar.StatusBarIcon; @@ -136,6 +138,8 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { }.setDuration(CONTENT_FADE_DURATION); private static final int MAX_ICONS_ON_AOD = 3; + + /* Maximum number of icons in short shelf on lockscreen when also showing overflow dot. */ public static final int MAX_ICONS_ON_LOCKSCREEN = 3; public static final int MAX_STATIC_ICONS = 4; private static final int MAX_DOTS = 1; @@ -145,7 +149,6 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { private int mDotPadding; private int mStaticDotRadius; private int mStaticDotDiameter; - private int mOverflowWidth; private int mActualLayoutWidth = NO_VALUE; private float mActualPaddingEnd = NO_VALUE; private float mActualPaddingStart = NO_VALUE; @@ -219,10 +222,6 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { paint.setColor(Color.RED); canvas.drawLine(mVisualOverflowStart, 0, mVisualOverflowStart, height, paint); - - paint.setColor(Color.YELLOW); - float overflow = getMaxOverflowStart(); - canvas.drawLine(overflow, 0, overflow, height, paint); } } @@ -255,14 +254,14 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { } } - private void setIconSize(int size) { + @VisibleForTesting + public void setIconSize(int size) { mIconSize = size; - mOverflowWidth = mIconSize + (MAX_DOTS - 1) * (mStaticDotDiameter + mDotPadding); } private void updateState() { resetViewStates(); - calculateIconTranslations(); + calculateIconXTranslations(); applyIconStates(); } @@ -390,12 +389,11 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { * @return Width of shelf for the given number of icons */ public float calculateWidthFor(float numIcons) { - if (getChildCount() == 0) { + if (numIcons == 0) { return 0f; } - final float contentWidth = numIcons <= MAX_ICONS_ON_LOCKSCREEN + 1 - ? numIcons * mIconSize - : MAX_ICONS_ON_LOCKSCREEN * mIconSize + (float) mOverflowWidth; + final float contentWidth = + mIconSize * MathUtils.min(numIcons, MAX_ICONS_ON_LOCKSCREEN + 1); return getActualPaddingStart() + contentWidth + getActualPaddingEnd(); @@ -406,14 +404,13 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { * are inserted into the notification container. * If this is not a whole number, the fraction means by how much the icon is appearing. */ - public void calculateIconTranslations() { + public void calculateIconXTranslations() { float translationX = getActualPaddingStart(); int firstOverflowIndex = -1; int childCount = getChildCount(); int maxVisibleIcons = mOnLockScreen ? MAX_ICONS_ON_AOD : mIsStaticLayout ? MAX_STATIC_ICONS : childCount; float layoutEnd = getLayoutEnd(); - float overflowStart = getMaxOverflowStart(); mVisualOverflowStart = 0; mFirstVisibleIconState = null; for (int i = 0; i < childCount; i++) { @@ -438,12 +435,12 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { ? StatusBarIconView.STATE_HIDDEN : StatusBarIconView.STATE_ICON; - boolean isOverflowing = - (translationX > (isLastChild ? layoutEnd - mIconSize - : overflowStart - mIconSize)); + final float overflowDotX = layoutEnd - mIconSize; + boolean isOverflowing = translationX > overflowDotX; + if (firstOverflowIndex == -1 && (forceOverflow || isOverflowing)) { firstOverflowIndex = isLastChild && !forceOverflow ? i - 1 : i; - mVisualOverflowStart = layoutEnd - mOverflowWidth; + mVisualOverflowStart = layoutEnd - mIconSize; if (forceOverflow || mIsStaticLayout) { mVisualOverflowStart = Math.min(translationX, mVisualOverflowStart); } @@ -477,7 +474,6 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { mLastVisibleIconState = mIconStates.get(lastChild); mFirstVisibleIconState = mIconStates.get(getChildAt(0)); } - if (isLayoutRtl()) { for (int i = 0; i < childCount; i++) { View view = getChildAt(i); @@ -568,7 +564,7 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { } private float getMaxOverflowStart() { - return getLayoutEnd() - mOverflowWidth; + return getLayoutEnd() - mIconSize; } public void setChangingViewPositions(boolean changingViewPositions) { @@ -635,7 +631,7 @@ public class NotificationIconContainer extends AlphaOptimizedFrameLayout { return 0; } - int collapsedPadding = mOverflowWidth; + int collapsedPadding = mIconSize; if (collapsedPadding + getFinalTranslationX() > getWidth()) { collapsedPadding = getWidth() - getFinalTranslationX(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt index c3658ba11b2dc..663490ebfde0f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackSizeCalculatorTest.kt @@ -49,7 +49,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { @Mock private lateinit var lockscreenShadeTransitionController: LockscreenShadeTransitionController @Mock private lateinit var stackLayout: NotificationStackScrollLayout - private val testableResources = mContext.getOrCreateTestableResources() + private val testableResources = mContext.orCreateTestableResources private lateinit var sizeCalculator: NotificationStackSizeCalculator @@ -121,17 +121,16 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { } @Test - fun computeHeight_returnsAtMostSpaceAvailable_withGapBeforeShelf() { + fun computeHeight_gapBeforeShelf_returnsSpaceUsed() { + // Each row in separate section. setGapHeight(gapHeight) - val shelfHeight = shelfHeight - val availableSpace = + val spaceUsed = listOf( - rowHeight + dividerHeight, - gapHeight + rowHeight + dividerHeight, - gapHeight + dividerHeight + shelfHeight) + dividerHeight + rowHeight, + dividerHeight + gapHeight + rowHeight, + dividerHeight + gapHeight + shelfHeight) .sum() - - // All rows in separate sections (default setup). + val availableSpace = spaceUsed + 1; val rows = listOf(createMockRow(rowHeight), createMockRow(rowHeight), createMockRow(rowHeight)) @@ -139,23 +138,29 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { assertThat(maxNotifications).isEqualTo(2) val height = sizeCalculator.computeHeight(stackLayout, maxNotifications, this.shelfHeight) - assertThat(height).isAtMost(availableSpace) + assertThat(height).isEqualTo(spaceUsed) } @Test - fun computeHeight_noGapBeforeShelf_returnsAtMostSpaceAvailable() { + fun computeHeight_noGapBeforeShelf_returnsSpaceUsed() { // Both rows are in the same section. setGapHeight(0f) + val rowHeight = rowHeight val shelfHeight = shelfHeight - val availableSpace = listOf(rowHeight + dividerHeight, dividerHeight + shelfHeight).sum() + val spaceUsed = + listOf( + dividerHeight + rowHeight, + dividerHeight + shelfHeight) + .sum() + val availableSpace = spaceUsed + 1 val rows = listOf(createMockRow(rowHeight), createMockRow(rowHeight)) val maxNotifications = computeMaxKeyguardNotifications(rows, availableSpace, shelfHeight) assertThat(maxNotifications).isEqualTo(1) val height = sizeCalculator.computeHeight(stackLayout, maxNotifications, this.shelfHeight) - assertThat(height).isAtMost(availableSpace) + assertThat(height).isEqualTo(spaceUsed) } @Test @@ -190,7 +195,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { val space = sizeCalculator.spaceNeeded(expandableView, visibleIndex = 0, previousView = null, stack = stackLayout, onLockscreen = true) - assertThat(space).isEqualTo(5) + assertThat(space).isEqualTo(5 + dividerHeight) } @Test @@ -204,7 +209,7 @@ class NotificationStackSizeCalculatorTest : SysuiTestCase() { val space = sizeCalculator.spaceNeeded(expandableView, visibleIndex = 0, previousView = null, stack = stackLayout, onLockscreen = false) - assertThat(space).isEqualTo(10) + assertThat(space).isEqualTo(10 + dividerHeight) } private fun computeMaxKeyguardNotifications( diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt new file mode 100644 index 0000000000000..2ff6dd43e84e0 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationIconContainerTest.kt @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2022 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 + +import android.service.notification.StatusBarNotification +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.StatusBarIconView +import junit.framework.Assert.assertEquals +import junit.framework.Assert.assertFalse +import junit.framework.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.Mockito.`when` as whenever + +/** + * Tests for {@link NotificationIconContainer}. + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotificationIconContainerTest : SysuiTestCase() { + + private val iconContainer = NotificationIconContainer(context, /* attrs= */ null) + + @Test + fun calculateWidthFor_zeroIcons_widthIsZero() { + assertEquals(/* expected= */ iconContainer.calculateWidthFor(/* numIcons= */ 0f), + /* actual= */ 0f) + } + + @Test + fun calculateWidthFor_oneIcon_widthForOneIcon() { + iconContainer.setActualPaddingStart(10f) + iconContainer.setActualPaddingEnd(10f) + iconContainer.setIconSize(10); + + assertEquals(/* expected= */ iconContainer.calculateWidthFor(/* numIcons= */ 1f), + /* actual= */ 30f) + } + + @Test + fun calculateWidthFor_fourIcons_widthForFourIcons() { + iconContainer.setActualPaddingStart(10f) + iconContainer.setActualPaddingEnd(10f) + iconContainer.setIconSize(10); + + assertEquals(/* expected= */ iconContainer.calculateWidthFor(/* numIcons= */ 4f), + /* actual= */ 60f) + } + + @Test + fun calculateWidthFor_fiveIcons_widthForFourIcons() { + iconContainer.setActualPaddingStart(10f) + iconContainer.setActualPaddingEnd(10f) + iconContainer.setIconSize(10); + assertEquals(/* expected= */ iconContainer.calculateWidthFor(/* numIcons= */ 5f), + /* actual= */ 60f) + } + + @Test + fun calculateIconXTranslations_shortShelfOneIcon_atCorrectXWithoutOverflowDot() { + iconContainer.setActualPaddingStart(10f) + iconContainer.setActualPaddingEnd(10f) + iconContainer.setIconSize(10); + + val icon = mockStatusBarIcon() + iconContainer.addView(icon) + assertEquals(1, iconContainer.childCount) + + val iconState = iconContainer.getIconState(icon) + iconState.iconAppearAmount = 1f + + val width = iconContainer.calculateWidthFor(/* numIcons= */ 1f) + iconContainer.setActualLayoutWidth(width.toInt()) + + iconContainer.calculateIconXTranslations() + assertEquals(10f, iconState.xTranslation) + assertFalse(iconContainer.hasOverflow()) + } + + @Test + fun calculateIconXTranslations_shortShelfFourIcons_atCorrectXWithoutOverflowDot() { + iconContainer.setActualPaddingStart(10f) + iconContainer.setActualPaddingEnd(10f) + iconContainer.setIconSize(10); + + val iconOne = mockStatusBarIcon() + val iconTwo = mockStatusBarIcon() + val iconThree = mockStatusBarIcon() + val iconFour = mockStatusBarIcon() + + iconContainer.addView(iconOne) + iconContainer.addView(iconTwo) + iconContainer.addView(iconThree) + iconContainer.addView(iconFour) + assertEquals(4, iconContainer.childCount) + + val width = iconContainer.calculateWidthFor(/* numIcons= */ 4f) + iconContainer.setActualLayoutWidth(width.toInt()) + + iconContainer.calculateIconXTranslations() + assertEquals(10f, iconContainer.getIconState(iconOne).xTranslation) + assertEquals(20f, iconContainer.getIconState(iconTwo).xTranslation) + assertEquals(30f, iconContainer.getIconState(iconThree).xTranslation) + assertEquals(40f, iconContainer.getIconState(iconFour).xTranslation) + + assertFalse(iconContainer.hasOverflow()) + } + + @Test + fun calculateIconXTranslations_shortShelfFiveIcons_atCorrectXWithOverflowDot() { + iconContainer.setActualPaddingStart(10f) + iconContainer.setActualPaddingEnd(10f) + iconContainer.setIconSize(10); + + val iconOne = mockStatusBarIcon() + val iconTwo = mockStatusBarIcon() + val iconThree = mockStatusBarIcon() + val iconFour = mockStatusBarIcon() + val iconFive = mockStatusBarIcon() + + iconContainer.addView(iconOne) + iconContainer.addView(iconTwo) + iconContainer.addView(iconThree) + iconContainer.addView(iconFour) + iconContainer.addView(iconFive) + assertEquals(5, iconContainer.childCount) + + val width = iconContainer.calculateWidthFor(/* numIcons= */ 5f) + iconContainer.setActualLayoutWidth(width.toInt()) + + iconContainer.calculateIconXTranslations() + assertEquals(10f, iconContainer.getIconState(iconOne).xTranslation) + assertEquals(20f, iconContainer.getIconState(iconTwo).xTranslation) + assertEquals(30f, iconContainer.getIconState(iconThree).xTranslation) + assertTrue(iconContainer.hasOverflow()) + } + + private fun mockStatusBarIcon() : StatusBarIconView { + val iconView = mock(StatusBarIconView::class.java) + whenever(iconView.width).thenReturn(10) + + val icon = mock(android.graphics.drawable.Icon::class.java) + whenever(iconView.sourceIcon).thenReturn(icon) + + val sbn = mock(StatusBarNotification::class.java) + whenever(sbn.groupKey).thenReturn("groupKey") + whenever(iconView.notification).thenReturn(sbn) + return iconView + } +} \ No newline at end of file