diff options
21 files changed, 470 insertions, 103 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index cda43476610e..cc8ffe2c35ef 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -397,8 +397,8 @@ private fun Modifier.debugBackground( } fun ShadeScrimRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape { - val topRadius = if (roundTop) radius else 0.dp - val bottomRadius = if (roundBottom) radius else 0.dp + val topRadius = if (isTopRounded) radius else 0.dp + val bottomRadius = if (isBottomRounded) radius else 0.dp return RoundedCornerShape( topStart = topRadius, topEnd = topRadius, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt index 53522e276112..c90b2319aae0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt @@ -31,6 +31,8 @@ import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds +import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape +import com.android.systemui.statusbar.notification.stack.shared.model.ViewPosition import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel import com.android.systemui.testKosmos @@ -64,20 +66,38 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { @Test fun updateBounds() = testScope.runTest { - val clipping by collectLastValue(appearanceViewModel.shadeScrimClipping) + val radius = MutableStateFlow(32) + val viewPosition = MutableStateFlow(ViewPosition(0, 0)) + val shape by collectLastValue(appearanceViewModel.shadeScrimShape(radius, viewPosition)) + + placeholderViewModel.onBoundsChanged(left = 0f, top = 200f, right = 100f, bottom = 550f) + assertThat(shape) + .isEqualTo( + ShadeScrimShape( + bounds = + ShadeScrimBounds(left = 0f, top = 200f, right = 100f, bottom = 550f), + topRadius = 32, + bottomRadius = 0 + ) + ) - val top = 200f - val left = 0f - val bottom = 550f - val right = 100f + viewPosition.value = ViewPosition(200, 15) + radius.value = 24 placeholderViewModel.onBoundsChanged( - left = left, - top = top, - right = right, - bottom = bottom + left = 210f, + top = 200f, + right = 300f, + bottom = 550f ) - assertThat(clipping?.bounds) - .isEqualTo(ShadeScrimBounds(left = left, top = top, right = right, bottom = bottom)) + assertThat(shape) + .isEqualTo( + ShadeScrimShape( + bounds = + ShadeScrimBounds(left = 10f, top = 185f, right = 100f, bottom = 535f), + topRadius = 24, + bottomRadius = 0 + ) + ) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt index dc928c49fbd3..50b77dcf9468 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt @@ -68,11 +68,11 @@ class NotificationStackAppearanceInteractorTest : SysuiTestCase() { kosmos.shadeRepository.setShadeMode(ShadeMode.Single) assertThat(stackRounding) - .isEqualTo(ShadeScrimRounding(roundTop = true, roundBottom = false)) + .isEqualTo(ShadeScrimRounding(isTopRounded = true, isBottomRounded = false)) kosmos.shadeRepository.setShadeMode(ShadeMode.Split) assertThat(stackRounding) - .isEqualTo(ShadeScrimRounding(roundTop = true, roundBottom = true)) + .isEqualTo(ShadeScrimRounding(isTopRounded = true, isBottomRounded = true)) } @Test(expected = IllegalStateException::class) diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt index ce798ba3912f..7dd883b89db7 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/ViewExt.kt @@ -17,6 +17,7 @@ package com.android.systemui.common.ui.view import android.view.View +import kotlinx.coroutines.DisposableHandle /** * Set this view's [View#importantForAccessibility] to [View#IMPORTANT_FOR_ACCESSIBILITY_YES] or @@ -43,3 +44,10 @@ inline fun <reified T : View> View.getNearestParent(): T? { } return null } + +/** Adds a [View.OnLayoutChangeListener] and provides a [DisposableHandle] for teardown. */ +fun View.onLayoutChanged(onLayoutChanged: (v: View) -> Unit): DisposableHandle { + val listener = View.OnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> onLayoutChanged(v) } + addOnLayoutChangeListener(listener) + return DisposableHandle { removeOnLayoutChangeListener(listener) } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 343f37744ce9..c9a4433fc5e5 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1588,7 +1588,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump * @param forceClockUpdate Should the clock be updated even when not on keyguard */ private void positionClockAndNotifications(boolean forceClockUpdate) { - boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); + boolean animate = !SceneContainerFlag.isEnabled() + && mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); int stackScrollerPadding; boolean onKeyguard = isKeyguardShowing(); @@ -1675,7 +1676,8 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mClockPositionResult.clockX, mClockPositionResult.clockY); } - boolean animate = mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); + boolean animate = !SceneContainerFlag.isEnabled() + && mNotificationStackScrollLayoutController.isAddOrRemoveAnimationPending(); boolean animateClock = (animate || mAnimateNextPositionUpdate) && shouldAnimateClockChange; if (!MigrateClocksToBlueprint.isEnabled()) { @@ -3963,7 +3965,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mShadeRepository.setLegacyShadeExpansion(mExpandedFraction); mQsController.setShadeExpansion(mExpandedHeight, mExpandedFraction); mExpansionDragDownAmountPx = h; - mAmbientState.setExpansionFraction(mExpandedFraction); + if (!SceneContainerFlag.isEnabled()) { + mAmbientState.setExpansionFraction(mExpandedFraction); + } onHeightUpdated(mExpandedHeight); updateExpansionAndVisibility(); }); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt index 2cb9f9acca26..fc84fa2051d0 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt @@ -43,6 +43,7 @@ import com.android.systemui.settings.UserTracker import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.NotificationInsetsController import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStackView import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.phone.KeyguardBottomAreaView import com.android.systemui.statusbar.phone.StatusBarLocation @@ -51,6 +52,7 @@ import com.android.systemui.statusbar.phone.TapAgainView import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.tuner.TunerService +import dagger.Binds import dagger.Module import dagger.Provides import javax.inject.Named @@ -59,6 +61,14 @@ import javax.inject.Provider /** Module for providing views related to the shade. */ @Module abstract class ShadeViewProviderModule { + + @Binds + @SysUISingleton + // TODO(b/277762009): Only allow this view's binder to inject the view. + abstract fun bindsNotificationStackView( + notificationStackScrollLayout: NotificationStackScrollLayout + ): NotificationStackView + companion object { const val SHADE_HEADER = "large_screen_shade_header" diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 3367dc427f43..a9a968fcdc07 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -113,6 +113,7 @@ import com.android.systemui.statusbar.notification.row.ExpandableView; import com.android.systemui.statusbar.notification.row.StackScrollerDecorView; import com.android.systemui.statusbar.notification.shared.NotificationsImprovedHunAnimation; import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor; +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStackView; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; import com.android.systemui.statusbar.phone.ScreenOffAnimationController; @@ -143,7 +144,9 @@ import java.util.function.Consumer; /** * A layout which handles a dynamic amount of notifications and presents them in a scrollable stack. */ -public class NotificationStackScrollLayout extends ViewGroup implements Dumpable { +public class NotificationStackScrollLayout + extends ViewGroup + implements Dumpable, NotificationStackView { public static final float BACKGROUND_ALPHA_DIMMED = 0.7f; private static final String TAG = "StackScroller"; @@ -218,6 +221,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable */ private final StackScrollAlgorithm mStackScrollAlgorithm; private final AmbientState mAmbientState; + private final StackViewFields mStackViewFields = new StackViewFields(); private final GroupMembershipManager mGroupMembershipManager; private final GroupExpansionManager mGroupExpansionManager; @@ -589,7 +593,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable @Override public boolean isScrolledToTop() { if (SceneContainerFlag.isEnabled()) { - return mController.isPlaceholderScrolledToTop(); + return mStackViewFields.isScrolledToTop(); } else { return mOwnScrollY == 0; } @@ -1130,6 +1134,48 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } } + @Override + public View asView() { + return this; + } + + @Override + public void setScrolledToTop(boolean scrolledToTop) { + mStackViewFields.setScrolledToTop(scrolledToTop); + } + + @Override + public void setStackTop(float stackTop) { + mStackViewFields.setStackTop(stackTop); + // TODO(b/332574413): replace the following with using stackTop + updateTopPadding(stackTop, isAddOrRemoveAnimationPending()); + } + + @Override + public void setStackBottom(float stackBottom) { + mStackViewFields.setStackBottom(stackBottom); + } + + @Override + public void setHeadsUpTop(float headsUpTop) { + mStackViewFields.setHeadsUpTop(headsUpTop); + } + + @Override + public void setSyntheticScrollConsumer(@Nullable Consumer<Float> consumer) { + mStackViewFields.setSyntheticScrollConsumer(consumer); + } + + @Override + public void setStackHeightConsumer(@Nullable Consumer<Float> consumer) { + mStackViewFields.setStackHeightConsumer(consumer); + } + + @Override + public void setHeadsUpHeightConsumer(@Nullable Consumer<Float> consumer) { + mStackViewFields.setHeadsUpHeightConsumer(consumer); + } + /** * @param listener to be notified after the location of Notification children might have * changed. @@ -1380,6 +1426,31 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable mOnStackYChanged = onStackYChanged; } + @Override + public void setExpandFraction(float expandFraction) { + if (SceneContainerFlag.isUnexpectedlyInLegacyMode()) return; + final float oldFraction = mAmbientState.getExpansionFraction(); + final boolean wasExpanding = oldFraction != 0f && oldFraction != 1f; + final boolean nowExpanding = expandFraction != 0f && expandFraction != 1f; + + // need to enter 'expanding' state before handling the new expand fraction, and then + if (nowExpanding && !wasExpanding) { + onExpansionStarted(); + mController.checkSnoozeLeavebehind(); + } + + // Update the expand progress between started/stopped events + mAmbientState.setExpansionFraction(expandFraction); + // TODO(b/332577544): don't convert to height which then converts to the fraction again + setExpandedHeight(expandFraction * getHeight()); + + // expansion stopped event requires that the expandFraction has already been updated + if (!nowExpanding && wasExpanding) { + setCheckForLeaveBehind(false); + onExpansionStopped(); + } + } + /** * Update the height of the panel. * @@ -2314,7 +2385,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable /* notificationStackScrollLayout= */ this, mMaxDisplayedNotifications, shelfIntrinsicHeight); mIntrinsicContentHeight = height; - mController.setIntrinsicContentHeight(mIntrinsicContentHeight); + mStackViewFields.sendStackHeight(height); // The topPadding can be bigger than the regular padding when qs is expanded, in that // state the maxPanelHeight and the contentHeight should be bigger @@ -3553,7 +3624,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable protected boolean isInsideQsHeader(MotionEvent ev) { if (SceneContainerFlag.isEnabled()) { - return ev.getY() < mController.getPlaceholderTop(); + return ev.getY() < mStackViewFields.getShadeScrimClipping().getBounds().getTop(); } mQsHeader.getBoundsOnScreen(mQsHeaderBound); @@ -4086,7 +4157,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable // to it so that it can scroll the stack and scrim accordingly. if (SceneContainerFlag.isEnabled()) { float diff = endPosition - layoutEnd; - mController.sendSyntheticScrollToSceneFramework(diff); + mStackViewFields.sendSyntheticScroll(diff); } setOwnScrollY((int) (mOwnScrollY + endPosition - layoutEnd)); mDisallowScrollingInThisMotion = true; @@ -4969,6 +5040,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable elapsedRealtime - mLastUpdateSidePaddingElapsedRealtime); println(pw, "isSmallLandscapeLockscreenEnabled", mIsSmallLandscapeLockscreenEnabled); mNotificationStackSizeCalculator.dump(pw, args); + mStackViewFields.dump(pw); }); pw.println(); pw.println("Contents:"); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 59901aca0425..06479e5a8e0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -83,6 +83,7 @@ import com.android.systemui.plugins.statusbar.NotificationSwipeActionHelper; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.scene.ui.view.WindowRootView; import com.android.systemui.shade.ShadeController; import com.android.systemui.shade.ShadeViewController; @@ -125,7 +126,6 @@ import com.android.systemui.statusbar.notification.row.NotificationGuts; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationSnooze; import com.android.systemui.statusbar.notification.shared.NotificationsHeadsUpRefactor; -import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.HeadsUpAppearanceController; import com.android.systemui.statusbar.phone.HeadsUpTouchHelper; @@ -189,7 +189,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; private final ShadeController mShadeController; private final Provider<WindowRootView> mWindowRootView; - private final NotificationStackAppearanceInteractor mStackAppearanceInteractor; private final KeyguardMediaController mKeyguardMediaController; private final SysuiStatusBarStateController mStatusBarStateController; private final KeyguardBypassController mKeyguardBypassController; @@ -742,7 +741,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { NotificationListViewBinder viewBinder, ShadeController shadeController, Provider<WindowRootView> windowRootView, - NotificationStackAppearanceInteractor stackAppearanceInteractor, InteractionJankMonitor jankMonitor, StackStateLogger stackLogger, NotificationStackScrollLogger logger, @@ -795,7 +793,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { mSeenNotificationsInteractor = seenNotificationsInteractor; mShadeController = shadeController; mWindowRootView = windowRootView; - mStackAppearanceInteractor = stackAppearanceInteractor; mFeatureFlags = featureFlags; mNotificationTargetsHelper = notificationTargetsHelper; mSecureSettings = secureSettings; @@ -1124,6 +1121,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public boolean isAddOrRemoveAnimationPending() { + SceneContainerFlag.assertInLegacyMode(); return mView != null && mView.isAddOrRemoveAnimationPending(); } @@ -1163,29 +1161,6 @@ public class NotificationStackScrollLayoutController implements Dumpable { } } - /** Send internal notification expansion to the scene container framework. */ - public void sendSyntheticScrollToSceneFramework(Float delta) { - mStackAppearanceInteractor.setSyntheticScroll(delta); - } - - /** Get the y-coordinate of the top bound of the stack. */ - public float getPlaceholderTop() { - return mStackAppearanceInteractor.getShadeScrimBounds().getValue().getTop(); - } - - /** - * Returns whether the notification stack is scrolled to the top; i.e., it cannot be scrolled - * down any further. - */ - public boolean isPlaceholderScrolledToTop() { - return mStackAppearanceInteractor.getScrolledToTop().getValue(); - } - - /** Set the intrinsic height of the stack content without additional padding. */ - public void setIntrinsicContentHeight(float intrinsicContentHeight) { - mStackAppearanceInteractor.setStackHeight(intrinsicContentHeight); - } - public void setIntrinsicPadding(int intrinsicPadding) { mView.setIntrinsicPadding(intrinsicPadding); } @@ -1264,6 +1239,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void setScrollingEnabled(boolean enabled) { + SceneContainerFlag.assertInLegacyMode(); mView.setScrollingEnabled(enabled); } @@ -1284,6 +1260,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void updateTopPadding(float qsHeight, boolean animate) { + SceneContainerFlag.assertInLegacyMode(); mView.updateTopPadding(qsHeight, animate); } @@ -1386,11 +1363,13 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void onExpansionStarted() { + SceneContainerFlag.assertInLegacyMode(); mView.onExpansionStarted(); checkSnoozeLeavebehind(); } public void onExpansionStopped() { + SceneContainerFlag.assertInLegacyMode(); mView.setCheckForLeaveBehind(false); mView.onExpansionStopped(); } @@ -1519,6 +1498,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void setExpandedHeight(float expandedHeight) { + SceneContainerFlag.assertInLegacyMode(); mView.setExpandedHeight(expandedHeight); } @@ -1794,6 +1774,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { */ public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius, int bottomRadius) { + SceneContainerFlag.assertInLegacyMode(); mView.setRoundedClippingBounds(left, top, right, bottom, topRadius, bottomRadius); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackViewFields.kt new file mode 100644 index 000000000000..5f62ae7b99f5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/StackViewFields.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 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.notification.stack + +import android.util.IndentingPrintWriter +import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping +import com.android.systemui.util.printSection +import com.android.systemui.util.println +import java.util.function.Consumer + +/** + * This is a state holder object used by [NSSL][NotificationStackScrollLayout] to contain states + * provided by the `NotificationStackViewBinder` to the [NotificationStackView] + * + * Unlike AmbientState, no class other than NSSL should ever have access to this class in any way. + * These fields are effectively NSSL's private fields. + */ +class StackViewFields { + /** Used to produce the clipping path */ + var shadeScrimClipping: ShadeScrimClipping = ShadeScrimClipping() + /** Y coordinate in view pixels of the top of the notification stack */ + var stackTop: Float = 0f + /** + * Y coordinate in view pixels above which the bottom of the notification stack / shelf / footer + * must be. + */ + var stackBottom: Float = 0f + /** Y coordinate in view pixels of the top of the HUN */ + var headsUpTop: Float = 0f + /** Whether the notifications are scrolled all the way to the top (i.e. when freshly opened) */ + var isScrolledToTop: Boolean = true + + /** + * When internal NSSL expansion requires the stack to be scrolled (e.g. to keep an expanding + * notification in view), that scroll amount can be sent here and it will be handled by the + * placeholder + */ + var syntheticScrollConsumer: Consumer<Float>? = null + /** + * Any time the stack height is recalculated, it should be updated here to be used by the + * placeholder + */ + var stackHeightConsumer: Consumer<Float>? = null + /** + * Any time the heads up height is recalculated, it should be updated here to be used by the + * placeholder + */ + var headsUpHeightConsumer: Consumer<Float>? = null + + /** send the [syntheticScroll] to the [syntheticScrollConsumer], if present. */ + fun sendSyntheticScroll(syntheticScroll: Float) = + syntheticScrollConsumer?.accept(syntheticScroll) + /** send the [stackHeight] to the [stackHeightConsumer], if present. */ + fun sendStackHeight(stackHeight: Float) = stackHeightConsumer?.accept(stackHeight) + /** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */ + fun sendHeadsUpHeight(headsUpHeight: Float) = headsUpHeightConsumer?.accept(headsUpHeight) + + fun dump(pw: IndentingPrintWriter) { + pw.printSection("StackViewStates") { + pw.println("shadeScrimClipping", shadeScrimClipping) + pw.println("stackTop", stackTop) + pw.println("stackBottom", stackBottom) + pw.println("headsUpTop", headsUpTop) + pw.println("isScrolledToTop", isScrolledToTop) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt index 938e43e4a2d4..8a9da69079d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt @@ -35,7 +35,7 @@ class NotificationViewHeightRepository @Inject constructor() { val stackHeight = MutableStateFlow(0f) /** The height in px of the current heads up notification. */ - val activeHeadsUpRowHeight = MutableStateFlow(0f) + val headsUpHeight = MutableStateFlow(0f) /** * The amount in px that the notification stack should scroll due to internal expansion. This diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 32562f109252..5753d612c9ec 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -59,8 +59,8 @@ constructor( isExpandingFromHeadsUp, ) { shadeMode, isExpandingFromHeadsUp -> ShadeScrimRounding( - roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp), - roundBottom = shadeMode != ShadeMode.Single, + isTopRounded = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp), + isBottomRounded = shadeMode != ShadeMode.Single, ) } .distinctUntilChanged() @@ -75,12 +75,18 @@ constructor( /** The y-coordinate in px of top of the contents of the notification stack. */ val stackTop: StateFlow<Float> = placeholderRepository.stackTop.asStateFlow() + /** The y-coordinate in px of bottom of the contents of the notification stack. */ + val stackBottom: StateFlow<Float> = placeholderRepository.stackBottom.asStateFlow() + /** * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any * further. */ val scrolledToTop: StateFlow<Boolean> = placeholderRepository.scrolledToTop.asStateFlow() + /** The y-coordinate in px of bottom of the contents of the HUN. */ + val headsUpTop: StateFlow<Float> = placeholderRepository.headsUpTop.asStateFlow() + /** * The amount in px that the notification stack should scroll due to internal expansion. This * should only happen when a notification expansion hits the bottom of the screen, so it is @@ -99,6 +105,11 @@ constructor( viewHeightRepository.stackHeight.value = height } + /** Sets the height of heads up notification. */ + fun setHeadsUpHeight(height: Float) { + viewHeightRepository.headsUpHeight.value = height + } + /** Sets the y-coord in px of the top of the contents of the notification stack. */ fun setStackTop(startY: Float) { placeholderRepository.stackTop.value = startY diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt index 448127a55b89..7e3e724bfed9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimBounds.kt @@ -29,4 +29,12 @@ data class ShadeScrimBounds( ) { /** The current height of the notification container. */ val height: Float = bottom - top + + operator fun minus(position: ViewPosition) = + ShadeScrimBounds( + left = left - position.left, + top = top - position.top, + right = right - position.left, + bottom = bottom - position.top, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt index 621dd0c49f54..e86fd1c7cdc7 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimClipping.kt @@ -16,7 +16,7 @@ package com.android.systemui.statusbar.notification.stack.shared.model -/** Models the clipping rounded rectangle of the notification stack */ +/** Models the clipping rounded rectangle of the notification stack, where the rounding is binary */ data class ShadeScrimClipping( val bounds: ShadeScrimBounds = ShadeScrimBounds(), val rounding: ShadeScrimRounding = ShadeScrimRounding() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt index 2fe265f35604..9d4231c8967b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimRounding.kt @@ -19,7 +19,7 @@ package com.android.systemui.statusbar.notification.stack.shared.model /** Models the corner rounds of the notification stack. */ data class ShadeScrimRounding( /** Whether the top corners of the notification stack should be rounded. */ - val roundTop: Boolean = false, + val isTopRounded: Boolean = false, /** Whether the bottom corners of the notification stack should be rounded. */ - val roundBottom: Boolean = false, + val isBottomRounded: Boolean = false, ) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimShape.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimShape.kt new file mode 100644 index 000000000000..e6f0647a059d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ShadeScrimShape.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2024 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.notification.stack.shared.model + +/** Models the clipping rounded rectangle of the notification stack, with corner radii in px */ +data class ShadeScrimShape( + val bounds: ShadeScrimBounds = ShadeScrimBounds(), + /** radius in px of the top corners */ + val topRadius: Int, + /** radius in px of the bottom corners */ + val bottomRadius: Int, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ViewPosition.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ViewPosition.kt new file mode 100644 index 000000000000..5d2b0ad2b0a6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/ViewPosition.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 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.notification.stack.shared.model + +/** An offset of view, used to adjust bounds. */ +data class ViewPosition(val left: Int = 0, val top: Int = 0) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStackView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStackView.kt new file mode 100644 index 000000000000..73f572d87af6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationStackView.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2024 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.notification.stack.ui.view + +import android.view.View +import java.util.function.Consumer + +/** + * This view (interface) is the view which scrolls and positions the heads up notification and + * notification stack, but is otherwise agnostic to the content. + */ +interface NotificationStackView { + /** + * Since this is an interface rather than a literal View, this provides cast-like access to the + * underlying view. + */ + fun asView(): View + + /** Set the clipping bounds used when drawing */ + fun setRoundedClippingBounds( + left: Int, + top: Int, + right: Int, + bottom: Int, + topRadius: Int, + bottomRadius: Int + ) + + /** set the y position in px of the top of the stack in this view's coordinates */ + fun setStackTop(stackTop: Float) + + /** set the y position in px of the bottom of the stack in this view's coordinates */ + fun setStackBottom(stackBottom: Float) + + /** set the y position in px of the top of the HUN in this view's coordinates */ + fun setHeadsUpTop(headsUpTop: Float) + + /** set whether the view has been scrolled all the way to the top */ + fun setScrolledToTop(scrolledToTop: Boolean) + + /** Set a consumer for synthetic scroll events */ + fun setSyntheticScrollConsumer(consumer: Consumer<Float>?) + /** Set a consumer for stack height changed events */ + fun setStackHeightConsumer(consumer: Consumer<Float>?) + /** Set a consumer for heads up height changed events */ + fun setHeadsUpHeightConsumer(consumer: Consumer<Float>?) + + /** sets that scrolling is allowed */ + fun setScrollingEnabled(enabled: Boolean) + + /** sets the current expand fraction */ + fun setExpandFraction(expandFraction: Float) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt index d6d31db86e61..a51e796db33e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt @@ -19,35 +19,44 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.common.ui.view.onLayoutChanged import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.res.R -import com.android.systemui.statusbar.notification.stack.AmbientState -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.shared.model.ViewPosition +import com.android.systemui.statusbar.notification.stack.ui.view.NotificationStackView import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel +import com.android.systemui.util.kotlin.FlowDumperImpl +import com.android.systemui.util.kotlin.launchAndDispose import javax.inject.Inject import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch -/** Binds the NSSL/Controller/AmbientState to their ViewModel. */ +/** Binds the NotificationStackView. */ @SysUISingleton class NotificationStackViewBinder @Inject constructor( + dumpManager: DumpManager, @Main private val mainImmediateDispatcher: CoroutineDispatcher, - private val ambientState: AmbientState, - private val view: NotificationStackScrollLayout, - private val controller: NotificationStackScrollLayoutController, + private val stack: NotificationStackView, private val viewModel: NotificationStackAppearanceViewModel, private val configuration: ConfigurationState, -) { +) : FlowDumperImpl(dumpManager) { + private val view = stack.asView() + + private val viewPosition = MutableStateFlow(ViewPosition()).dumpValue("viewPosition") + private val viewTopOffset = viewPosition.map { it.top }.distinctUntilChanged() fun bindWhileAttached(): DisposableHandle { return view.repeatWhenAttached(mainImmediateDispatcher) { @@ -56,48 +65,48 @@ constructor( } suspend fun bind() = coroutineScope { - launch { - combine(viewModel.shadeScrimClipping, clipRadius, ::Pair).collect { - (clipping, clipRadius) -> - val (bounds, rounding) = clipping - val viewLeft = controller.view.left - val viewTop = controller.view.top - controller.setRoundedClippingBounds( - bounds.left.roundToInt() - viewLeft, - bounds.top.roundToInt() - viewTop, - bounds.right.roundToInt() - viewLeft, - bounds.bottom.roundToInt() - viewTop, - if (rounding.roundTop) clipRadius else 0, - if (rounding.roundBottom) clipRadius else 0, - ) - } + launchAndDispose { + viewPosition.value = ViewPosition(view.left, view.top) + view.onLayoutChanged { viewPosition.value = ViewPosition(it.left, it.top) } } launch { - viewModel.stackTop.collect { - controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending) + viewModel.shadeScrimShape(scrimRadius, viewPosition).collect { clipping -> + stack.setRoundedClippingBounds( + clipping.bounds.left.roundToInt(), + clipping.bounds.top.roundToInt(), + clipping.bounds.right.roundToInt(), + clipping.bounds.bottom.roundToInt(), + clipping.topRadius, + clipping.bottomRadius, + ) } } - launch { - var wasExpanding = false - viewModel.expandFraction.collect { expandFraction -> - val nowExpanding = expandFraction != 0f && expandFraction != 1f - if (nowExpanding && !wasExpanding) { - controller.onExpansionStarted() - } - ambientState.expansionFraction = expandFraction - controller.expandedHeight = expandFraction * controller.view.height - if (!nowExpanding && wasExpanding) { - controller.onExpansionStopped() - } - wasExpanding = nowExpanding + launch { viewModel.stackTop.minusTopOffset().collect { stack.setStackTop(it) } } + launch { viewModel.stackBottom.minusTopOffset().collect { stack.setStackBottom(it) } } + launch { viewModel.scrolledToTop.collect { stack.setScrolledToTop(it) } } + launch { viewModel.headsUpTop.minusTopOffset().collect { stack.setHeadsUpTop(it) } } + launch { viewModel.expandFraction.collect { stack.setExpandFraction(it) } } + launch { viewModel.isScrollable.collect { stack.setScrollingEnabled(it) } } + + launchAndDispose { + stack.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) + stack.setStackHeightConsumer(viewModel.stackHeightConsumer) + stack.setHeadsUpHeightConsumer(viewModel.headsUpHeightConsumer) + DisposableHandle { + stack.setSyntheticScrollConsumer(null) + stack.setStackHeightConsumer(null) + stack.setHeadsUpHeightConsumer(null) } } - - launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } } } - private val clipRadius: Flow<Int> + /** Combine with the topOffset flow and subtract that value from this flow's value */ + private fun Flow<Float>.minusTopOffset() = + combine(viewTopOffset) { y, topOffset -> y - topOffset } + + /** flow of the scrim clipping radius */ + private val scrimRadius: Flow<Int> get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt index 071127c03202..b5776f30d938 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt @@ -26,10 +26,11 @@ import com.android.systemui.scene.shared.model.Scenes.Shade import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimClipping +import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape +import com.android.systemui.statusbar.notification.stack.shared.model.ViewPosition import com.android.systemui.util.kotlin.FlowDumperImpl import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map @@ -80,7 +81,7 @@ constructor( .dumpWhileCollecting("expandFraction") /** The bounds of the notification stack in the current scene. */ - val shadeScrimClipping: Flow<ShadeScrimClipping> = + private val shadeScrimClipping: Flow<ShadeScrimClipping> = combine( stackAppearanceInteractor.shadeScrimBounds, stackAppearanceInteractor.shadeScrimRounding, @@ -88,8 +89,35 @@ constructor( ) .dumpWhileCollecting("stackClipping") + fun shadeScrimShape(cornerRadius: Flow<Int>, viewPosition: Flow<ViewPosition>) = + combine(shadeScrimClipping, cornerRadius, viewPosition) { clipping, radius, position -> + ShadeScrimShape( + bounds = clipping.bounds - position, + topRadius = radius.takeIf { clipping.rounding.isTopRounded } ?: 0, + bottomRadius = radius.takeIf { clipping.rounding.isBottomRounded } ?: 0 + ) + } + .dumpWhileCollecting("shadeScrimShape") + /** The y-coordinate in px of top of the contents of the notification stack. */ - val stackTop: StateFlow<Float> = stackAppearanceInteractor.stackTop.dumpValue("stackTop") + val stackTop: Flow<Float> = stackAppearanceInteractor.stackTop.dumpValue("stackTop") + /** The y-coordinate in px of bottom of the contents of the notification stack. */ + val stackBottom: Flow<Float> = stackAppearanceInteractor.stackBottom.dumpValue("stackBottom") + /** + * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any + * further. + */ + val scrolledToTop: Flow<Boolean> = + stackAppearanceInteractor.scrolledToTop.dumpValue("scrolledToTop") + /** The y-coordinate in px of bottom of the contents of the HUN. */ + val headsUpTop: Flow<Float> = stackAppearanceInteractor.headsUpTop.dumpValue("headsUpTop") + + /** Receives the amount (px) that the stack should scroll due to internal expansion. */ + val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll + /** Receives the height of the contents of the notification stack. */ + val stackHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setStackHeight + /** Receives the height of the heads up notification. */ + val headsUpHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setHeadsUpHeight /** Whether the notification stack is scrollable or not. */ val isScrollable: Flow<Boolean> = diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt index 909a18be4b9e..97d957dde01b 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandleExt.kt @@ -17,8 +17,14 @@ package com.android.systemui.util.kotlin import com.android.systemui.lifecycle.repeatWhenAttached +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.Job import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.launch /** * Suspends to keep getting updates until cancellation. Once cancelled, mark this as eligible for @@ -42,3 +48,22 @@ suspend fun DisposableHandle.awaitCancellationThenDispose() { dispose() } } + +/** + * This will [launch], run [onLaunch] to get a [DisposableHandle], and finally + * [awaitCancellationThenDispose][DisposableHandle.awaitCancellationThenDispose]. This can be used + * to structure self-disposing code which attaches listeners, for example in ViewBinders: + * ``` + * suspend fun bind(view: MyView, viewModel: MyViewModel) = coroutineScope { + * launchAndDispose { + * view.setOnClickListener { viewModel.handleClick() } + * DisposableHandle { view.setOnClickListener(null) } + * } + * } + * ``` + */ +inline fun CoroutineScope.launchAndDispose( + context: CoroutineContext = EmptyCoroutineContext, + start: CoroutineStart = CoroutineStart.DEFAULT, + crossinline onLaunch: () -> DisposableHandle +): Job = launch(context, start) { onLaunch().awaitCancellationThenDispose() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index cd8be573a2ac..912ecb340c3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -94,7 +94,6 @@ import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController.NotificationPanelEvent; import com.android.systemui.statusbar.notification.stack.NotificationSwipeHelper.NotificationCallback; -import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor; import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationListViewBinder; import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -161,7 +160,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private VisibilityLocationProviderDelegator mVisibilityLocationProviderDelegator; @Mock private ShadeController mShadeController; @Mock private Provider<WindowRootView> mWindowRootView; - @Mock private NotificationStackAppearanceInteractor mNotificationStackAppearanceInteractor; private final StackStateLogger mStackLogger = new StackStateLogger(logcatLogBuffer(), logcatLogBuffer()); private final NotificationStackScrollLogger mLogger = new NotificationStackScrollLogger( @@ -1016,7 +1014,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { mViewBinder, mShadeController, mWindowRootView, - mNotificationStackAppearanceInteractor, mKosmos.getInteractionJankMonitor(), mStackLogger, mLogger, |