diff options
| author | 2024-03-24 13:43:52 +0000 | |
|---|---|---|
| committer | 2024-04-04 01:59:24 +0000 | |
| commit | fdf1804b42698e8ca3dcffb75bcd8461f85b1d6d (patch) | |
| tree | 0e5500b8c297054f14e95a203bcdf5078d88c2a1 | |
| parent | d83844f80f841c5ae51283d1f282cd1fbec7f97a (diff) | |
[flexiglass] Add NotificationStackView (NSV) interface
* NSV is implemented by NSSL so that we can eventually stop injecting NSSL/NSSLC everywhere.
* Remove the NotificationStackAppearanceInteractor from the NSSLC and bind all fields to NSV instead of NSSLC.
* Add SceneContainerFlag.assertInLegacyMode() to some NSSLC methods where the new code can go directly to NSV.
* Add ShadeScrimShape which includes the actual rounding radii, and have the ViewModel take the radius flow and position flow to expose that directly, moving that ViewBinder logic to the ViewModel.
Bug: 296118689
Test: atest SystemUITests
Flag: ACONFIG com.android.systemui.scene_container DEVELOPMENT
Change-Id: I87e6a5219c46f0cdbed569e004a016d6064fcc8f
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, |