diff options
16 files changed, 561 insertions, 69 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java index de334bbd880c..2338be28d32c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/EmptyShadeView.java @@ -85,7 +85,9 @@ public class EmptyShadeView extends StackScrollerDecorView { public void setFooterVisibility(@Visibility int visibility) { mFooterVisibility = visibility; - setSecondaryVisible(visibility == View.VISIBLE, false); + setSecondaryVisible(/* visible = */ visibility == View.VISIBLE, + /* animate = */false, + /* onAnimationEnded = */ null); } public void setFooterText(@StringRes int text) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt index 19bce8988840..fa2748c1dc77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinator.kt @@ -23,6 +23,7 @@ import com.android.systemui.statusbar.notification.collection.coordinator.dagger import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor @@ -41,6 +42,7 @@ internal constructor( private val groupExpansionManagerImpl: GroupExpansionManagerImpl, private val notificationIconAreaController: NotificationIconAreaController, private val renderListInteractor: RenderNotificationListInteractor, + private val activeNotificationsInteractor: ActiveNotificationsInteractor, ) : Coordinator { override fun attach(pipeline: NotifPipeline) { @@ -50,7 +52,13 @@ internal constructor( fun onAfterRenderList(entries: List<ListEntry>, controller: NotifStackController) = traceSection("StackCoordinator.onAfterRenderList") { - controller.setNotifStats(calculateNotifStats(entries)) + val notifStats = calculateNotifStats(entries) + if (FooterViewRefactor.isEnabled) { + activeNotificationsInteractor.setNotifStats(notifStats) + } + // TODO(b/293167744): This shouldn't be done if the footer flag is on, once the footer + // visibility is handled in the new stack. + controller.setNotifStats(notifStats) if (NotificationIconContainerRefactor.isEnabled || FooterViewRefactor.isEnabled) { renderListInteractor.setRenderedList(entries) } else { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt index 12ee54d4977d..5ed82cc1ed5c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/ActiveNotificationListRepository.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.notification.data.repository import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.notification.collection.render.NotifStats import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore.Key import com.android.systemui.statusbar.notification.shared.ActiveNotificationEntryModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel @@ -37,6 +38,9 @@ class ActiveNotificationListRepository @Inject constructor() { /** Are any already-seen notifications currently filtered out of the active list? */ val hasFilteredOutSeenNotifications = MutableStateFlow(false) + + /** Stats about the list of notifications attached to the shade */ + val notifStats = MutableStateFlow(NotifStats.empty) } /** Represents the notification list, comprised of groups and individual notifications. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt index 542f3c4c414a..31893b402e3c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt @@ -15,6 +15,7 @@ package com.android.systemui.statusbar.notification.domain.interactor +import com.android.systemui.statusbar.notification.collection.render.NotifStats import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.shared.ActiveNotificationGroupModel import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel @@ -52,4 +53,14 @@ constructor( */ val areAnyNotificationsPresentValue: Boolean get() = repository.activeNotifications.value.renderList.isNotEmpty() + + /** Are there are any notifications that can be cleared by the "Clear all" button? */ + val hasClearableNotifications: Flow<Boolean> = + repository.notifStats + .map { it.hasClearableAlertingNotifs || it.hasClearableSilentNotifs } + .distinctUntilChanged() + + fun setNotifStats(notifStats: NotifStats) { + repository.notifStats.value = notifStats + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java index 10a43d53353d..3184d5efe5cf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterView.java @@ -46,6 +46,7 @@ import com.android.systemui.statusbar.notification.stack.ViewState; import com.android.systemui.util.DumpUtilsKt; import java.io.PrintWriter; +import java.util.function.Consumer; public class FooterView extends StackScrollerDecorView { private static final String TAG = "FooterView"; @@ -63,9 +64,13 @@ public class FooterView extends StackScrollerDecorView { private String mSeenNotifsFilteredText; private Drawable mSeenNotifsFilteredIcon; + private @StringRes int mClearAllButtonTextId; + private @StringRes int mClearAllButtonDescriptionId; private @StringRes int mMessageStringId; private @DrawableRes int mMessageIconId; + private OnClickListener mClearAllButtonClickListener; + public FooterView(Context context, AttributeSet attrs) { super(context, attrs); } @@ -84,12 +89,18 @@ public class FooterView extends StackScrollerDecorView { return isSecondaryVisible(); } + /** See {@link this#setClearAllButtonVisible(boolean, boolean, Consumer)}. */ + public void setClearAllButtonVisible(boolean visible, boolean animate) { + setClearAllButtonVisible(visible, animate, /* onAnimationEnded = */ null); + } + /** * Set the visibility of the "Clear all" button to {@code visible}. Animate the change if * {@code animate} is true. */ - public void setClearAllButtonVisible(boolean visible, boolean animate) { - setSecondaryVisible(visible, animate); + public void setClearAllButtonVisible(boolean visible, boolean animate, + Consumer<Boolean> onAnimationEnded) { + setSecondaryVisible(visible, animate, onAnimationEnded); } @Override @@ -106,6 +117,42 @@ public class FooterView extends StackScrollerDecorView { }); } + /** Set the text label for the "Clear all" button. */ + public void setClearAllButtonText(@StringRes int textId) { + if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return; + if (mClearAllButtonTextId == textId) { + return; // nothing changed + } + mClearAllButtonTextId = textId; + updateClearAllButtonText(); + } + + private void updateClearAllButtonText() { + if (mClearAllButtonTextId == 0) { + return; // not initialized yet + } + mClearAllButton.setText(getContext().getString(mClearAllButtonTextId)); + } + + /** Set the accessibility content description for the "Clear all" button. */ + public void setClearAllButtonDescription(@StringRes int contentDescriptionId) { + if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) { + return; + } + if (mClearAllButtonDescriptionId == contentDescriptionId) { + return; // nothing changed + } + mClearAllButtonDescriptionId = contentDescriptionId; + updateClearAllButtonDescription(); + } + + private void updateClearAllButtonDescription() { + if (mClearAllButtonDescriptionId == 0) { + return; // not initialized yet + } + mClearAllButton.setContentDescription(getContext().getString(mClearAllButtonDescriptionId)); + } + /** Set the string for a message to be shown instead of the buttons. */ public void setMessageString(@StringRes int messageId) { if (FooterViewRefactor.isUnexpectedlyInLegacyMode()) return; @@ -181,6 +228,10 @@ public class FooterView extends StackScrollerDecorView { /** Set onClickListener for the clear all (end) button. */ public void setClearAllButtonClickListener(OnClickListener listener) { + if (FooterViewRefactor.isEnabled()) { + if (mClearAllButtonClickListener == listener) return; + mClearAllButtonClickListener = listener; + } mClearAllButton.setOnClickListener(listener); } @@ -214,7 +265,28 @@ public class FooterView extends StackScrollerDecorView { mManageButton.setText(mManageNotificationText); mManageButton.setContentDescription(mManageNotificationText); } - if (!FooterViewRefactor.isEnabled()) { + if (FooterViewRefactor.isEnabled()) { + updateClearAllButtonText(); + updateClearAllButtonDescription(); + + updateMessageString(); + updateMessageIcon(); + } else { + // NOTE: Prior to the refactor, `updateResources` set the class properties to the right + // string values. It was always being called together with `updateContent`, which + // deals with actually associating those string values with the correct views + // (buttons or text). + // In the new code, the resource IDs are being set in the view binder (through + // setMessageString and similar setters). The setters themselves now deal with + // updating both the resource IDs and the views where appropriate (as in, calling + // `updateMessageString` when the resource ID changes). This eliminates the need for + // `updateResources`, which will eventually be removed. There are, however, still + // situations in which we want to update the views even if the resource IDs didn't + // change, such as configuration changes. + mClearAllButton.setText(R.string.clear_all_notifications_text); + mClearAllButton.setContentDescription( + mContext.getString(R.string.accessibility_clear_all)); + mSeenNotifsFooterTextView.setText(mSeenNotifsFilteredText); mSeenNotifsFooterTextView .setCompoundDrawablesRelative(mSeenNotifsFilteredIcon, null, null, null); @@ -230,16 +302,8 @@ public class FooterView extends StackScrollerDecorView { protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); updateColors(); - mClearAllButton.setText(R.string.clear_all_notifications_text); - mClearAllButton.setContentDescription( - mContext.getString(R.string.accessibility_clear_all)); updateResources(); updateContent(); - - if (FooterViewRefactor.isEnabled()) { - updateMessageString(); - updateMessageIcon(); - } } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt index 6d8234371b65..0299114e0afc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewbinder/FooterViewBinder.kt @@ -16,10 +16,14 @@ package com.android.systemui.statusbar.notification.footer.ui.viewbinder +import android.view.View import androidx.lifecycle.lifecycleScope import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.statusbar.notification.footer.ui.view.FooterView import com.android.systemui.statusbar.notification.footer.ui.viewmodel.FooterViewModel +import com.android.systemui.util.ui.isAnimating +import com.android.systemui.util.ui.stopAnimating +import com.android.systemui.util.ui.value import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.launch @@ -28,9 +32,31 @@ object FooterViewBinder { fun bind( footer: FooterView, viewModel: FooterViewModel, + clearAllNotifications: View.OnClickListener, ): DisposableHandle { + // Listen for changes when the view is attached. return footer.repeatWhenAttached { - // Listen for changes when the view is attached. + lifecycleScope.launch { + viewModel.clearAllButton.collect { button -> + if (button.isVisible.isAnimating) { + footer.setClearAllButtonVisible( + button.isVisible.value, + /* animate = */ true, + ) { _ -> + button.isVisible.stopAnimating() + } + } else { + footer.setClearAllButtonVisible( + button.isVisible.value, + /* animate = */ false, + ) + } + footer.setClearAllButtonText(button.labelId) + footer.setClearAllButtonDescription(button.accessibilityDescriptionId) + footer.setClearAllButtonClickListener(clearAllNotifications) + } + } + lifecycleScope.launch { viewModel.message.collect { message -> footer.setFooterLabelVisible(message.visible) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt new file mode 100644 index 000000000000..ea5abeff7042 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterButtonViewModel.kt @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.footer.ui.viewmodel + +import android.annotation.StringRes +import com.android.systemui.util.ui.AnimatedValue + +data class FooterButtonViewModel( + @StringRes val labelId: Int, + @StringRes val accessibilityDescriptionId: Int, + val isVisible: AnimatedValue<Boolean>, +) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt index 3d68a7b6f9b9..721bea1086e6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModel.kt @@ -18,18 +18,51 @@ package com.android.systemui.statusbar.notification.footer.ui.viewmodel import com.android.systemui.dagger.SysUISingleton import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor import com.android.systemui.statusbar.notification.footer.ui.view.FooterView +import com.android.systemui.util.kotlin.sample +import com.android.systemui.util.ui.AnimatableEvent +import com.android.systemui.util.ui.toAnimatedValueFlow import dagger.Module import dagger.Provides import java.util.Optional import javax.inject.Provider import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart /** ViewModel for [FooterView]. */ -class FooterViewModel(seenNotificationsInteractor: SeenNotificationsInteractor) { +class FooterViewModel( + activeNotificationsInteractor: ActiveNotificationsInteractor, + seenNotificationsInteractor: SeenNotificationsInteractor, + shadeInteractor: ShadeInteractor, +) { + val clearAllButton: Flow<FooterButtonViewModel> = + activeNotificationsInteractor.hasClearableNotifications + .sample( + combine( + shadeInteractor.isShadeFullyExpanded, + shadeInteractor.isShadeTouchable, + ::Pair + ) + .onStart { emit(Pair(false, false)) } + ) { hasClearableNotifications, (isShadeFullyExpanded, animationsEnabled) -> + val shouldAnimate = isShadeFullyExpanded && animationsEnabled + AnimatableEvent(hasClearableNotifications, shouldAnimate) + } + .toAnimatedValueFlow() + .map { visible -> + FooterButtonViewModel( + labelId = R.string.clear_all_notifications_text, + accessibilityDescriptionId = R.string.accessibility_clear_all, + isVisible = visible, + ) + } + val message: Flow<FooterMessageViewModel> = seenNotificationsInteractor.hasFilteredOutSeenNotifications.map { hasFilteredOutNotifs -> FooterMessageViewModel( @@ -45,10 +78,18 @@ object FooterViewModelModule { @Provides @SysUISingleton fun provideOptional( + activeNotificationsInteractor: Provider<ActiveNotificationsInteractor>, seenNotificationsInteractor: Provider<SeenNotificationsInteractor>, + shadeInteractor: Provider<ShadeInteractor>, ): Optional<FooterViewModel> { return if (FooterViewRefactor.isEnabled) { - Optional.of(FooterViewModel(seenNotificationsInteractor.get())) + Optional.of( + FooterViewModel( + activeNotificationsInteractor.get(), + seenNotificationsInteractor.get(), + shadeInteractor.get() + ) + ) } else { Optional.empty() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java index ec90a8d6ad59..162e8af47394 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/StackScrollerDecorView.java @@ -54,7 +54,7 @@ public abstract class StackScrollerDecorView extends ExpandableView { mContent = findContentView(); mSecondaryView = findSecondaryView(); setVisible(false /* visible */, false /* animate */); - setSecondaryVisible(false /* visible */, false /* animate */); + setSecondaryVisible(false /* visible */, false /* animate */, null /* onAnimationEnd */); setOutlineProvider(null); } @@ -155,15 +155,23 @@ public abstract class StackScrollerDecorView extends ExpandableView { /** * Set the secondary view of this layout to visible. * - * @param visible should the secondary view be visible - * @param animate should the change be animated + * @param visible True if the contents should be visible. + * @param animate True if we should fade to new visibility. + * @param onAnimationEnded Callback to run after visibility updates, takes a boolean as a + * parameter that represents whether the animation was cancelled. */ - protected void setSecondaryVisible(boolean visible, boolean animate) { + protected void setSecondaryVisible(boolean visible, boolean animate, + Consumer<Boolean> onAnimationEnded) { if (mIsSecondaryVisible != visible) { mSecondaryAnimating = animate; mIsSecondaryVisible = visible; - setViewVisible(mSecondaryView, visible, animate, - (cancelled) -> onSecondaryVisibilityAnimationEnd()); + Consumer<Boolean> onAnimationEndedWrapper = (cancelled) -> { + onContentVisibilityAnimationEnd(); + if (onAnimationEnded != null) { + onAnimationEnded.accept(cancelled); + } + }; + setViewVisible(mSecondaryView, visible, animate, onAnimationEndedWrapper); } if (!mSecondaryAnimating) { 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 6a34f980dde3..283a5930f930 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 @@ -4608,13 +4608,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable if (mManageButtonClickListener != null) { mFooterView.setManageButtonClickListener(mManageButtonClickListener); } - mFooterView.setClearAllButtonClickListener(v -> { - if (mFooterClearAllListener != null) { - mFooterClearAllListener.onClearAll(); - } - clearNotifications(ROWS_ALL, true /* closeShade */); - footerView.setClearAllButtonVisible(false /* visible */, true /* animate */); - }); + if (!FooterViewRefactor.isEnabled()) { + mFooterView.setClearAllButtonClickListener(v -> { + if (mFooterClearAllListener != null) { + mFooterClearAllListener.onClearAll(); + } + clearNotifications(ROWS_ALL, true /* closeShade */); + footerView.setClearAllButtonVisible(false /* visible */, true /* animate */); + }); + } if (FooterViewRefactor.isEnabled()) { updateFooter(); } @@ -4687,9 +4689,9 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } boolean animate = mIsExpanded && mAnimationsEnabled; mFooterView.setVisible(visible, animate); - mFooterView.setClearAllButtonVisible(showDismissView, animate); mFooterView.showHistory(showHistory); if (!FooterViewRefactor.isEnabled()) { + mFooterView.setClearAllButtonVisible(showDismissView, animate); mFooterView.setFooterLabelVisible(mHasFilteredOutSeenNotifications); } } @@ -5355,11 +5357,15 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable return viewsToRemove; } + /** Clear all clearable notifications when the user requests it. */ + public void clearAllNotifications() { + clearNotifications(ROWS_ALL, /* closeShade = */ true); + } + /** * Collects a list of visible rows, and animates them away in a staggered fashion as if they * were dismissed. Notifications are dismissed in the backend via onClearAllAnimationsEnd. */ - @VisibleForTesting void clearNotifications(@SelectedRows int selection, boolean closeShade) { // Animate-swipe all dismissable notifications, then animate the shade closed final ArrayList<View> viewsToAnimateAway = getVisibleViewsToAnimateAway(selection); @@ -5659,6 +5665,7 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable } void setFooterClearAllListener(FooterClearAllListener listener) { + FooterViewRefactor.assertInLegacyMode(); mFooterClearAllListener = listener; } 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 d5ec0c552f83..e6315fd159d5 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 @@ -470,7 +470,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { @Override public void onSnooze(StatusBarNotification sbn, - NotificationSwipeActionHelper.SnoozeOption snoozeOption) { + NotificationSwipeActionHelper.SnoozeOption snoozeOption) { mNotificationsController.setNotificationSnoozed(sbn, snoozeOption); } @@ -592,7 +592,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { @Override public boolean updateSwipeProgress(View animView, boolean dismissable, - float swipeProgress) { + float swipeProgress) { // Returning true prevents alpha fading. return false; } @@ -759,8 +759,10 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setClearAllAnimationListener(this::onAnimationEnd); mView.setClearAllListener((selection) -> mUiEventLogger.log( NotificationPanelEvent.fromSelection(selection))); - mView.setFooterClearAllListener(() -> - mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES)); + if (!FooterViewRefactor.isEnabled()) { + mView.setFooterClearAllListener(() -> + mMetricsLogger.action(MetricsEvent.ACTION_DISMISS_ALL_NOTES)); + } mView.setIsRemoteInputActive(mRemoteInputManager.isRemoteInputActive()); mRemoteInputManager.addControllerCallback(new RemoteInputController.Callback() { @Override @@ -1090,7 +1092,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } public void setOverScrollAmount(float amount, boolean onTop, boolean animate, - boolean cancelAnimators) { + boolean cancelAnimators) { mView.setOverScrollAmount(amount, onTop, animate, cancelAnimators); } @@ -1408,14 +1410,14 @@ public class NotificationStackScrollLayoutController implements Dumpable { * Return whether there are any clearable notifications */ public boolean hasActiveClearableNotifications(@SelectedRows int selection) { - // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the clear all - // button in the refactored code + // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer + // visibility in the refactored code return hasNotifications(selection, true /* clearable */); } public boolean hasNotifications(@SelectedRows int selection, boolean isClearable) { - // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the clear all - // button in the refactored code + // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once we handle the footer + // visibility in the refactored code boolean hasAlertingMatchingClearable = isClearable ? mNotifStats.getHasClearableAlertingNotifs() : mNotifStats.getHasNonClearableAlertingNotifs(); @@ -1454,7 +1456,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { public RemoteInputController.Delegate createDelegate() { return new RemoteInputController.Delegate() { public void setRemoteInputActive(NotificationEntry entry, - boolean remoteInputActive) { + boolean remoteInputActive) { mHeadsUpManager.setRemoteInputActive(entry, remoteInputActive); entry.notifyHeightChanged(true /* needsAnimation */); updateFooter(); @@ -1573,7 +1575,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { } private void onAnimationEnd(List<ExpandableNotificationRow> viewsToRemove, - @SelectedRows int selectedRows) { + @SelectedRows int selectedRows) { if (selectedRows == ROWS_ALL) { mNotifCollection.dismissAllNotifications( mLockscreenUserManager.getCurrentUserId()); @@ -1665,7 +1667,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { * Set rounded rect clipping bounds on this view. */ public void setRoundedClippingBounds(int left, int top, int right, int bottom, int topRadius, - int bottomRadius) { + int bottomRadius) { mView.setRoundedClippingBounds(left, top, right, bottom, topRadius, bottomRadius); } @@ -2021,7 +2023,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { private class NotifStackControllerImpl implements NotifStackController { @Override public void setNotifStats(@NonNull NotifStats notifStats) { - // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once clear all visibility + // TODO(b/293167744): FooterViewRefactor.assertInLegacyMode() once footer visibility // is handled in the refactored stack. mNotifStats = notifStats; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt index 54df4abe8b13..4554085c35c0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationListViewBinder.kt @@ -19,6 +19,8 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.view.LayoutInflater import androidx.lifecycle.lifecycleScope import com.android.app.tracing.traceSection +import com.android.internal.logging.MetricsLogger +import com.android.internal.logging.nano.MetricsProto import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.common.ui.reinflateAndBindLatest import com.android.systemui.common.ui.view.setImportantForAccessibilityYesNo @@ -46,6 +48,7 @@ class NotificationListViewBinder @Inject constructor( private val viewModel: NotificationListViewModel, + private val metricsLogger: MetricsLogger, private val configuration: ConfigurationState, private val configurationController: ConfigurationController, private val falsingManager: FalsingManager, @@ -100,7 +103,17 @@ constructor( attachToRoot = false, ) { footerView: FooterView -> traceSection("bind FooterView") { - val disposableHandle = FooterViewBinder.bind(footerView, footerViewModel) + val disposableHandle = + FooterViewBinder.bind( + footerView, + footerViewModel, + clearAllNotifications = { + metricsLogger.action( + MetricsProto.MetricsEvent.ACTION_DISMISS_ALL_NOTES + ) + parentView.clearAllNotifications() + }, + ) parentView.setFooterView(footerView) return@reinflateAndBindLatest disposableHandle } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt index 428574bb15f8..fa5fad06b671 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/StackCoordinatorTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.OnAfte import com.android.systemui.statusbar.notification.collection.render.GroupExpansionManagerImpl import com.android.systemui.statusbar.notification.collection.render.NotifStackController import com.android.systemui.statusbar.notification.collection.render.NotifStats +import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.RenderNotificationListInteractor import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.stack.BUCKET_ALERTING @@ -57,6 +58,7 @@ class StackCoordinatorTest : SysuiTestCase() { @Mock private lateinit var groupExpansionManagerImpl: GroupExpansionManagerImpl @Mock private lateinit var notificationIconAreaController: NotificationIconAreaController @Mock private lateinit var renderListInteractor: RenderNotificationListInteractor + @Mock private lateinit var activeNotificationsInteractor: ActiveNotificationsInteractor @Mock private lateinit var stackController: NotifStackController @Mock private lateinit var section: NotifSection @@ -75,6 +77,7 @@ class StackCoordinatorTest : SysuiTestCase() { groupExpansionManagerImpl, notificationIconAreaController, renderListInteractor, + activeNotificationsInteractor, ) coordinator.attach(pipeline) afterRenderListListener = withArgCaptor { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt index b24cafdfdc84..4ab3cd49b297 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractorTest.kt @@ -72,4 +72,58 @@ class ActiveNotificationsInteractorTest : SysuiTestCase() { assertThat(areAnyNotificationsPresent).isFalse() assertThat(underTest.areAnyNotificationsPresentValue).isFalse() } + + @Test + fun testHasClearableNotifications_whenHasClearableAlertingNotifs() = + testComponent.runTest { + val hasClearable by collectLastValue(underTest.hasClearableNotifications) + + activeNotificationListRepository.notifStats.value = + NotifStats( + numActiveNotifs = 2, + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = true, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = false, + ) + runCurrent() + + assertThat(hasClearable).isTrue() + } + + @Test + fun testHasClearableNotifications_whenHasClearableSilentNotifs() = + testComponent.runTest { + val hasClearable by collectLastValue(underTest.hasClearableNotifications) + + activeNotificationListRepository.notifStats.value = + NotifStats( + numActiveNotifs = 2, + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = false, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = true, + ) + runCurrent() + + assertThat(hasClearable).isTrue() + } + + @Test + fun testHasClearableNotifications_whenHasNoClearableNotifs() = + testComponent.runTest { + val hasClearable by collectLastValue(underTest.hasClearableNotifications) + + activeNotificationListRepository.notifStats.value = + NotifStats( + numActiveNotifs = 2, + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = false, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = false, + ) + runCurrent() + + assertThat(hasClearable).isFalse() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java index a64ac674a91c..22c5bae93489 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java @@ -114,9 +114,46 @@ public class FooterViewTest extends SysuiTestCase { } @Test + public void testSetClearAllButtonText_resourceOnlyFetchedOnce() { + int resId = R.string.clear_all_notifications_text; + mView.setClearAllButtonText(resId); + verify(mSpyContext).getString(eq(resId)); + + clearInvocations(mSpyContext); + + assertThat(((TextView) mView.findViewById(R.id.dismiss_text)) + .getText().toString()).contains("Clear all"); + + // Set it a few more times, it shouldn't lead to the resource being fetched again + mView.setClearAllButtonText(resId); + mView.setClearAllButtonText(resId); + + verify(mSpyContext, never()).getString(anyInt()); + } + + @Test + public void testSetClearAllButtonDescription_resourceOnlyFetchedOnce() { + int resId = R.string.accessibility_clear_all; + mView.setClearAllButtonDescription(resId); + verify(mSpyContext).getString(eq(resId)); + + clearInvocations(mSpyContext); + + assertThat(((TextView) mView.findViewById(R.id.dismiss_text)) + .getContentDescription().toString()).contains("Clear all notifications"); + + // Set it a few more times, it shouldn't lead to the resource being fetched again + mView.setClearAllButtonDescription(resId); + mView.setClearAllButtonDescription(resId); + + verify(mSpyContext, never()).getString(anyInt()); + } + + @Test public void testSetMessageString_resourceOnlyFetchedOnce() { - mView.setMessageString(R.string.unlock_to_see_notif_text); - verify(mSpyContext).getString(eq(R.string.unlock_to_see_notif_text)); + int resId = R.string.unlock_to_see_notif_text; + mView.setMessageString(resId); + verify(mSpyContext).getString(eq(resId)); clearInvocations(mSpyContext); @@ -124,22 +161,23 @@ public class FooterViewTest extends SysuiTestCase { .getText().toString()).contains("Unlock"); // Set it a few more times, it shouldn't lead to the resource being fetched again - mView.setMessageString(R.string.unlock_to_see_notif_text); - mView.setMessageString(R.string.unlock_to_see_notif_text); + mView.setMessageString(resId); + mView.setMessageString(resId); verify(mSpyContext, never()).getString(anyInt()); } @Test public void testSetMessageIcon_resourceOnlyFetchedOnce() { - mView.setMessageIcon(R.drawable.ic_friction_lock_closed); - verify(mSpyContext).getDrawable(eq(R.drawable.ic_friction_lock_closed)); + int resId = R.drawable.ic_friction_lock_closed; + mView.setMessageIcon(resId); + verify(mSpyContext).getDrawable(eq(resId)); clearInvocations(mSpyContext); // Set it a few more times, it shouldn't lead to the resource being fetched again - mView.setMessageIcon(R.drawable.ic_friction_lock_closed); - mView.setMessageIcon(R.drawable.ic_friction_lock_closed); + mView.setMessageIcon(resId); + mView.setMessageIcon(resId); verify(mSpyContext, never()).getDrawable(anyInt()); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt index 57a7c3c7e2bf..94dcf7a18514 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/footer/ui/viewmodel/FooterViewModelTest.kt @@ -18,37 +18,222 @@ package com.android.systemui.statusbar.notification.footer.ui.viewmodel import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysUITestComponent +import com.android.systemui.SysUITestModule import com.android.systemui.SysuiTestCase -import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.TestMocksModule +import com.android.systemui.collectLastValue +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.flags.FakeFeatureFlagsClassicModule +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.power.data.repository.FakePowerRepository +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.runCurrent +import com.android.systemui.runTest +import com.android.systemui.shade.data.repository.FakeShadeRepository +import com.android.systemui.statusbar.notification.collection.render.NotifStats import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository -import com.android.systemui.statusbar.notification.domain.interactor.SeenNotificationsInteractor +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ActivatableNotificationViewModelModule +import com.android.systemui.statusbar.phone.DozeParameters +import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeModule +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.ui.isAnimating +import com.android.systemui.util.ui.value import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.runTest +import dagger.BindsInstance +import dagger.Component +import java.util.Optional +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.MockitoAnnotations @RunWith(AndroidTestingRunner::class) @SmallTest class FooterViewModelTest : SysuiTestCase() { - private val repository = ActiveNotificationListRepository() - private val interactor = SeenNotificationsInteractor(repository) - private val underTest = FooterViewModel(interactor) + private lateinit var footerViewModel: FooterViewModel - @Test - fun testMessageVisible_whenFilteredNotifications() = runTest { - val message by collectLastValue(underTest.message) + @SysUISingleton + @Component( + modules = + [ + SysUITestModule::class, + ActivatableNotificationViewModelModule::class, + FooterViewModelModule::class, + HeadlessSystemUserModeModule::class, + ] + ) + interface TestComponent : SysUITestComponent<Optional<FooterViewModel>> { + val activeNotificationListRepository: ActiveNotificationListRepository + val configurationRepository: FakeConfigurationRepository + val keyguardRepository: FakeKeyguardRepository + val keyguardTransitionRepository: FakeKeyguardTransitionRepository + val shadeRepository: FakeShadeRepository + val powerRepository: FakePowerRepository + + @Component.Factory + interface Factory { + fun create( + @BindsInstance test: SysuiTestCase, + featureFlags: FakeFeatureFlagsClassicModule, + mocks: TestMocksModule, + ): TestComponent + } + } + + private val dozeParameters: DozeParameters = mock() - repository.hasFilteredOutSeenNotifications.value = true + private val testComponent: TestComponent = + DaggerFooterViewModelTest_TestComponent.factory() + .create( + test = this, + featureFlags = + FakeFeatureFlagsClassicModule { + set(com.android.systemui.flags.Flags.FULL_SCREEN_USER_SWITCHER, true) + }, + mocks = + TestMocksModule( + dozeParameters = dozeParameters, + ) + ) - assertThat(message?.visible).isTrue() + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + mSetFlagsRule.enableFlags(Flags.FLAG_NOTIFICATIONS_FOOTER_VIEW_REFACTOR) + + // The underTest in the component is Optional, because that matches the provider we + // currently have for the footer view model. + footerViewModel = testComponent.underTest.get() } @Test - fun testMessageVisible_whenNoFilteredNotifications() = runTest { - val message by collectLastValue(underTest.message) + fun testMessageVisible_whenFilteredNotifications() = + testComponent.runTest { + val message by collectLastValue(footerViewModel.message) - repository.hasFilteredOutSeenNotifications.value = false + activeNotificationListRepository.hasFilteredOutSeenNotifications.value = true - assertThat(message?.visible).isFalse() - } + assertThat(message?.visible).isTrue() + } + + @Test + fun testMessageVisible_whenNoFilteredNotifications() = + testComponent.runTest { + val message by collectLastValue(footerViewModel.message) + + activeNotificationListRepository.hasFilteredOutSeenNotifications.value = false + + assertThat(message?.visible).isFalse() + } + + @Test + fun testClearAllButtonVisible_whenHasClearableNotifs() = + testComponent.runTest { + val button by collectLastValue(footerViewModel.clearAllButton) + + activeNotificationListRepository.notifStats.value = + NotifStats( + numActiveNotifs = 2, + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = true, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = true, + ) + runCurrent() + + assertThat(button?.isVisible?.value).isTrue() + } + + @Test + fun testClearAllButtonVisible_whenHasNoClearableNotifs() = + testComponent.runTest { + val button by collectLastValue(footerViewModel.clearAllButton) + + activeNotificationListRepository.notifStats.value = + NotifStats( + numActiveNotifs = 2, + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = false, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = false, + ) + runCurrent() + + assertThat(button?.isVisible?.value).isFalse() + } + + @Test + fun testClearAllButtonAnimating_whenShadeExpandedAndTouchable() = + testComponent.runTest { + val button by collectLastValue(footerViewModel.clearAllButton) + runCurrent() + + // WHEN shade is expanded + keyguardRepository.setStatusBarState(StatusBarState.SHADE) + shadeRepository.setLegacyShadeExpansion(1f) + // AND QS not expanded + shadeRepository.setQsExpansion(0f) + // AND device is awake + powerRepository.updateWakefulness( + rawState = WakefulnessState.AWAKE, + lastWakeReason = WakeSleepReason.POWER_BUTTON, + lastSleepReason = WakeSleepReason.OTHER, + ) + runCurrent() + + // AND there are clearable notifications + activeNotificationListRepository.notifStats.value = + NotifStats( + numActiveNotifs = 2, + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = true, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = true, + ) + runCurrent() + + // THEN button visibility should animate + assertThat(button?.isVisible?.isAnimating).isTrue() + } + + @Test + fun testClearAllButtonAnimating_whenShadeNotExpanded() = + testComponent.runTest { + val button by collectLastValue(footerViewModel.clearAllButton) + runCurrent() + + // WHEN shade is collapsed + keyguardRepository.setStatusBarState(StatusBarState.SHADE) + shadeRepository.setLegacyShadeExpansion(0f) + // AND QS not expanded + shadeRepository.setQsExpansion(0f) + // AND device is awake + powerRepository.updateWakefulness( + rawState = WakefulnessState.AWAKE, + lastWakeReason = WakeSleepReason.POWER_BUTTON, + lastSleepReason = WakeSleepReason.OTHER, + ) + runCurrent() + + // AND there are clearable notifications + activeNotificationListRepository.notifStats.value = + NotifStats( + numActiveNotifs = 2, + hasNonClearableAlertingNotifs = false, + hasClearableAlertingNotifs = true, + hasNonClearableSilentNotifs = false, + hasClearableSilentNotifs = true, + ) + runCurrent() + + // THEN button visibility should not animate + assertThat(button?.isVisible?.isAnimating).isFalse() + } } |