diff options
3 files changed, 314 insertions, 86 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt index 2047c62cc5a9..ee797274deac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/VisualInterruptionSuppressor.kt @@ -73,6 +73,11 @@ abstract class VisualInterruptionCondition( override val uiEventId: UiEventEnum? = null, override val eventLogData: EventLogData? = null ) : VisualInterruptionSuppressor { + constructor( + types: Set<VisualInterruptionType>, + reason: String + ) : this(types, reason, /* uiEventId = */ null) + /** @return true if these interruptions should be suppressed right now. */ abstract fun shouldSuppress(): Boolean } @@ -84,6 +89,11 @@ abstract class VisualInterruptionFilter( override val uiEventId: UiEventEnum? = null, override val eventLogData: EventLogData? = null ) : VisualInterruptionSuppressor { + constructor( + types: Set<VisualInterruptionType>, + reason: String + ) : this(types, reason, /* uiEventId = */ null) + /** * @param entry the notification to consider suppressing * @return true if these interruptions should be suppressed for this notification right now diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java index 07e2571bcb38..8e9c0384987d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenter.java @@ -14,6 +14,9 @@ package com.android.systemui.statusbar.phone; +import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE; +import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK; +import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE; import static com.android.systemui.statusbar.phone.CentralSurfaces.CLOSE_PANEL_WHEN_EMPTIED; import static com.android.systemui.statusbar.phone.CentralSurfaces.DEBUG; @@ -29,6 +32,8 @@ import android.util.Log; import android.util.Slog; import android.view.View; +import androidx.annotation.NonNull; + import com.android.internal.statusbar.IStatusBarService; import com.android.systemui.InitController; import com.android.systemui.dagger.SysUISingleton; @@ -54,7 +59,10 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource; import com.android.systemui.statusbar.notification.domain.interactor.NotificationAlertsInteractor; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptSuppressor; +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionCondition; import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider; +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionFilter; +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.row.NotificationGutsManager.OnSettingsClickListener; @@ -63,6 +71,8 @@ import com.android.systemui.statusbar.notification.stack.NotificationStackScroll import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; +import java.util.Set; + import javax.inject.Inject; @SysUISingleton @@ -163,7 +173,14 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu initController.addPostInitTask(() -> { mNotifShadeEventSource.setShadeEmptiedCallback(this::maybeClosePanelForShadeEmptied); mNotifShadeEventSource.setNotifRemovedByUserCallback(this::maybeEndAmbientPulse); - visualInterruptionDecisionProvider.addLegacySuppressor(mInterruptSuppressor); + if (VisualInterruptionRefactor.isEnabled()) { + visualInterruptionDecisionProvider.addCondition(mAlertsDisabledCondition); + visualInterruptionDecisionProvider.addCondition(mVrModeCondition); + visualInterruptionDecisionProvider.addFilter(mNeedsRedactionFilter); + visualInterruptionDecisionProvider.addCondition(mPanelsDisabledCondition); + } else { + visualInterruptionDecisionProvider.addLegacySuppressor(mInterruptSuppressor); + } mLockscreenUserManager.setUpWithPresenter(this); mGutsManager.setUpWithPresenter( this, mNotifListContainer, mOnSettingsClickListener); @@ -306,4 +323,54 @@ class StatusBarNotificationPresenter implements NotificationPresenter, CommandQu return !mNotificationAlertsInteractor.areNotificationAlertsEnabled(); } }; + + private final VisualInterruptionCondition mAlertsDisabledCondition = + new VisualInterruptionCondition(Set.of(PEEK, PULSE, BUBBLE), + "notification alerts disabled") { + @Override + public boolean shouldSuppress() { + return !mNotificationAlertsInteractor.areNotificationAlertsEnabled(); + } + }; + + private final VisualInterruptionCondition mVrModeCondition = + new VisualInterruptionCondition(Set.of(PEEK, BUBBLE), "device is in VR mode") { + @Override + public boolean shouldSuppress() { + return isDeviceInVrMode(); + } + }; + + private final VisualInterruptionFilter mNeedsRedactionFilter = + new VisualInterruptionFilter(Set.of(PEEK), "needs redaction on public lockscreen") { + @Override + public boolean shouldSuppress(@NonNull NotificationEntry entry) { + if (!mKeyguardStateController.isOccluded()) { + return false; + } + + if (!mLockscreenUserManager.needsRedaction(entry)) { + return false; + } + + final int currentUserId = mLockscreenUserManager.getCurrentUserId(); + final boolean currentUserPublic = mLockscreenUserManager.isLockscreenPublicMode( + currentUserId); + + final int notificationUserId = entry.getSbn().getUserId(); + final boolean notificationUserPublic = + mLockscreenUserManager.isLockscreenPublicMode(notificationUserId); + + // TODO(b/135046837): we can probably relax this with dynamic privacy + return currentUserPublic || notificationUserPublic; + } + }; + + private final VisualInterruptionCondition mPanelsDisabledCondition = + new VisualInterruptionCondition(Set.of(PEEK), "disabled panel") { + @Override + public boolean shouldSuppress() { + return !mCommandQueue.panelsEnabled(); + } + }; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java index 53c621d24601..bbdc9ced57ee 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationPresenterTest.java @@ -16,22 +16,28 @@ package com.android.systemui.statusbar.phone; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.BUBBLE; +import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PEEK; +import static com.android.systemui.statusbar.notification.interruption.VisualInterruptionType.PULSE; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; import android.app.StatusBarManager; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.testing.TestableLooper.RunWithLooper; import androidx.test.filters.SmallTest; -import com.android.internal.logging.testing.FakeMetricsLogger; +import com.android.systemui.Flags; import com.android.systemui.InitController; import com.android.systemui.SysuiTestCase; import com.android.systemui.plugins.ActivityStarter; @@ -55,7 +61,10 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource; import com.android.systemui.statusbar.notification.domain.interactor.NotificationAlertsInteractor; import com.android.systemui.statusbar.notification.interruption.NotificationInterruptSuppressor; +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionCondition; import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider; +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionFilter; +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionType; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout; @@ -64,10 +73,14 @@ import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.KeyguardStateController; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import java.util.List; +import java.util.Set; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper() @@ -76,18 +89,23 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase { private final VisualInterruptionDecisionProvider mVisualInterruptionDecisionProvider = mock(VisualInterruptionDecisionProvider.class); private NotificationInterruptSuppressor mInterruptSuppressor; + private VisualInterruptionCondition mAlertsDisabledCondition; + private VisualInterruptionCondition mVrModeCondition; + private VisualInterruptionFilter mNeedsRedactionFilter; + private VisualInterruptionCondition mPanelsDisabledCondition; private CommandQueue mCommandQueue; - private FakeMetricsLogger mMetricsLogger; private final ShadeController mShadeController = mock(ShadeController.class); private final NotificationAlertsInteractor mNotificationAlertsInteractor = mock(NotificationAlertsInteractor.class); private final KeyguardStateController mKeyguardStateController = mock(KeyguardStateController.class); - private final InitController mInitController = new InitController(); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule( + SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT); @Before public void setup() { - mMetricsLogger = new FakeMetricsLogger(); mCommandQueue = new CommandQueue(mContext, new FakeDisplayTracker(mContext)); mDependency.injectTestDependency(StatusBarStateController.class, mock(SysuiStatusBarStateController.class)); @@ -95,15 +113,182 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase { mDependency.injectMockDependency(NotificationRemoteInputManager.Callback.class); mDependency.injectMockDependency(NotificationShadeWindowController.class); - NotificationShadeWindowView notificationShadeWindowView = + when(mNotificationAlertsInteractor.areNotificationAlertsEnabled()).thenReturn(true); + } + + @Test + public void testInit_refactorDisabled() { + ensureRefactorDisabledState(); + } + + @Test + public void testInit_refactorEnabled() { + ensureRefactorEnabledState(); + } + + @Test + public void testNoSuppressHeadsUp_default_refactorDisabled() { + ensureRefactorDisabledState(); + + assertFalse(mInterruptSuppressor.suppressAwakeHeadsUp(createNotificationEntry())); + } + + @Test + public void testNoSuppressHeadsUp_default_refactorEnabled() { + ensureRefactorEnabledState(); + + assertFalse(mAlertsDisabledCondition.shouldSuppress()); + assertFalse(mVrModeCondition.shouldSuppress()); + assertFalse(mNeedsRedactionFilter.shouldSuppress(createNotificationEntry())); + assertFalse(mAlertsDisabledCondition.shouldSuppress()); + } + + @Test + public void testSuppressHeadsUp_disabledStatusBar_refactorDisabled() { + ensureRefactorDisabledState(); + + mCommandQueue.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_EXPAND, 0, + false /* animate */); + TestableLooper.get(this).processAllMessages(); + + assertTrue("The panel should suppress heads up while disabled", + mInterruptSuppressor.suppressAwakeHeadsUp(createNotificationEntry())); + } + + @Test + public void testSuppressHeadsUp_disabledStatusBar_refactorEnabled() { + ensureRefactorEnabledState(); + + mCommandQueue.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_EXPAND, 0, + false /* animate */); + TestableLooper.get(this).processAllMessages(); + + assertTrue("The panel should suppress heads up while disabled", + mPanelsDisabledCondition.shouldSuppress()); + } + + @Test + public void testSuppressHeadsUp_disabledNotificationShade_refactorDisabled() { + ensureRefactorDisabledState(); + + mCommandQueue.disable(DEFAULT_DISPLAY, 0, StatusBarManager.DISABLE2_NOTIFICATION_SHADE, + false /* animate */); + TestableLooper.get(this).processAllMessages(); + + assertTrue("The panel should suppress interruptions while notification shade disabled", + mInterruptSuppressor.suppressAwakeHeadsUp(createNotificationEntry())); + } + + @Test + public void testSuppressHeadsUp_disabledNotificationShade_refactorEnabled() { + ensureRefactorEnabledState(); + + mCommandQueue.disable(DEFAULT_DISPLAY, 0, StatusBarManager.DISABLE2_NOTIFICATION_SHADE, + false /* animate */); + TestableLooper.get(this).processAllMessages(); + + assertTrue("The panel should suppress interruptions while notification shade disabled", + mPanelsDisabledCondition.shouldSuppress()); + } + + @Test + public void testPanelsDisabledConditionSuppressesPeek() { + ensureRefactorEnabledState(); + + final Set<VisualInterruptionType> types = mPanelsDisabledCondition.getTypes(); + assertTrue(types.contains(PEEK)); + assertFalse(types.contains(PULSE)); + assertFalse(types.contains(BUBBLE)); + } + + @Test + public void testNoSuppressHeadsUp_FSI_nonOccludedKeyguard_refactorDisabled() { + ensureRefactorDisabledState(); + + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(false); + + assertFalse(mInterruptSuppressor.suppressAwakeHeadsUp(createFsiNotificationEntry())); + } + + @Test + public void testNoSuppressHeadsUp_FSI_nonOccludedKeyguard_refactorEnabled() { + ensureRefactorEnabledState(); + + when(mKeyguardStateController.isShowing()).thenReturn(true); + when(mKeyguardStateController.isOccluded()).thenReturn(false); + + assertFalse(mNeedsRedactionFilter.shouldSuppress(createFsiNotificationEntry())); + + final Set<VisualInterruptionType> types = mNeedsRedactionFilter.getTypes(); + assertTrue(types.contains(PEEK)); + assertFalse(types.contains(PULSE)); + assertFalse(types.contains(BUBBLE)); + } + + @Test + public void testSuppressInterruptions_vrMode_refactorDisabled() { + ensureRefactorDisabledState(); + + mStatusBarNotificationPresenter.mVrMode = true; + + assertTrue("Vr mode should suppress interruptions", + mInterruptSuppressor.suppressAwakeInterruptions(createNotificationEntry())); + } + + @Test + public void testSuppressInterruptions_vrMode_refactorEnabled() { + ensureRefactorEnabledState(); + + mStatusBarNotificationPresenter.mVrMode = true; + + assertTrue("Vr mode should suppress interruptions", mVrModeCondition.shouldSuppress()); + + final Set<VisualInterruptionType> types = mVrModeCondition.getTypes(); + assertTrue(types.contains(PEEK)); + assertFalse(types.contains(PULSE)); + assertTrue(types.contains(BUBBLE)); + } + + @Test + public void testSuppressInterruptions_statusBarAlertsDisabled_refactorDisabled() { + ensureRefactorDisabledState(); + + when(mNotificationAlertsInteractor.areNotificationAlertsEnabled()).thenReturn(false); + + assertTrue("When alerts aren't enabled, interruptions are suppressed", + mInterruptSuppressor.suppressInterruptions(createNotificationEntry())); + } + + @Test + public void testSuppressInterruptions_statusBarAlertsDisabled_refactorEnabled() { + ensureRefactorEnabledState(); + + when(mNotificationAlertsInteractor.areNotificationAlertsEnabled()).thenReturn(false); + + assertTrue("When alerts aren't enabled, interruptions are suppressed", + mAlertsDisabledCondition.shouldSuppress()); + + final Set<VisualInterruptionType> types = mAlertsDisabledCondition.getTypes(); + assertTrue(types.contains(PEEK)); + assertTrue(types.contains(PULSE)); + assertTrue(types.contains(BUBBLE)); + } + + private void createPresenter() { + final ShadeViewController shadeViewController = mock(ShadeViewController.class); + + final NotificationShadeWindowView notificationShadeWindowView = mock(NotificationShadeWindowView.class); + when(notificationShadeWindowView.getResources()).thenReturn(mContext.getResources()); + NotificationStackScrollLayoutController stackScrollLayoutController = mock(NotificationStackScrollLayoutController.class); when(stackScrollLayoutController.getView()).thenReturn( mock(NotificationStackScrollLayout.class)); - when(notificationShadeWindowView.getResources()).thenReturn(mContext.getResources()); - ShadeViewController shadeViewController = mock(ShadeViewController.class); + final InitController initController = new InitController(); + mStatusBarNotificationPresenter = new StatusBarNotificationPresenter( mContext, shadeViewController, @@ -125,110 +310,76 @@ public class StatusBarNotificationPresenterTest extends SysuiTestCase { mock(NotifShadeEventSource.class), mock(NotificationMediaManager.class), mock(NotificationGutsManager.class), - mInitController, + initController, mVisualInterruptionDecisionProvider, mock(NotificationRemoteInputManager.class), mock(NotificationRemoteInputManager.Callback.class), mock(NotificationListContainer.class)); - mInitController.executePostInitTasks(); - ArgumentCaptor<NotificationInterruptSuppressor> suppressorCaptor = - ArgumentCaptor.forClass(NotificationInterruptSuppressor.class); - verify(mVisualInterruptionDecisionProvider).addLegacySuppressor(suppressorCaptor.capture()); - mInterruptSuppressor = suppressorCaptor.getValue(); + + initController.executePostInitTasks(); } - @Test - public void testNoSuppressHeadsUp_default() { - Notification n = new Notification.Builder(getContext(), "a").build(); - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg("a") - .setOpPkg("a") - .setTag("a") - .setNotification(n) - .build(); + private void verifyAndCaptureSuppressors() { + mInterruptSuppressor = null; - assertFalse(mInterruptSuppressor.suppressAwakeHeadsUp(entry)); + final ArgumentCaptor<VisualInterruptionCondition> conditionCaptor = + ArgumentCaptor.forClass(VisualInterruptionCondition.class); + verify(mVisualInterruptionDecisionProvider, times(3)).addCondition( + conditionCaptor.capture()); + final List<VisualInterruptionCondition> conditions = conditionCaptor.getAllValues(); + mAlertsDisabledCondition = conditions.get(0); + mVrModeCondition = conditions.get(1); + mPanelsDisabledCondition = conditions.get(2); + + final ArgumentCaptor<VisualInterruptionFilter> needsRedactionFilterCaptor = + ArgumentCaptor.forClass(VisualInterruptionFilter.class); + verify(mVisualInterruptionDecisionProvider).addFilter(needsRedactionFilterCaptor.capture()); + mNeedsRedactionFilter = needsRedactionFilterCaptor.getValue(); } - @Test - public void testSuppressHeadsUp_disabledStatusBar() { - Notification n = new Notification.Builder(getContext(), "a").build(); - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg("a") - .setOpPkg("a") - .setTag("a") - .setNotification(n) - .build(); - mCommandQueue.disable(DEFAULT_DISPLAY, StatusBarManager.DISABLE_EXPAND, 0, - false /* animate */); - TestableLooper.get(this).processAllMessages(); + private void verifyAndCaptureLegacySuppressor() { + mAlertsDisabledCondition = null; + mVrModeCondition = null; + mNeedsRedactionFilter = null; + mPanelsDisabledCondition = null; - assertTrue("The panel should suppress heads up while disabled", - mInterruptSuppressor.suppressAwakeHeadsUp(entry)); + final ArgumentCaptor<NotificationInterruptSuppressor> suppressorCaptor = + ArgumentCaptor.forClass(NotificationInterruptSuppressor.class); + verify(mVisualInterruptionDecisionProvider).addLegacySuppressor(suppressorCaptor.capture()); + mInterruptSuppressor = suppressorCaptor.getValue(); } - @Test - public void testSuppressHeadsUp_disabledNotificationShade() { - Notification n = new Notification.Builder(getContext(), "a").build(); - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg("a") - .setOpPkg("a") - .setTag("a") - .setNotification(n) - .build(); - mCommandQueue.disable(DEFAULT_DISPLAY, 0, StatusBarManager.DISABLE2_NOTIFICATION_SHADE, - false /* animate */); - TestableLooper.get(this).processAllMessages(); + private void ensureRefactorDisabledState() { + mSetFlagsRule.disableFlags(Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR); + createPresenter(); + verifyAndCaptureLegacySuppressor(); + } - assertTrue("The panel should suppress interruptions while notification shade " - + "disabled", - mInterruptSuppressor.suppressAwakeHeadsUp(entry)); + private void ensureRefactorEnabledState() { + mSetFlagsRule.enableFlags(Flags.FLAG_VISUAL_INTERRUPTIONS_REFACTOR); + createPresenter(); + verifyAndCaptureSuppressors(); } - @Test - public void testNoSuppressHeadsUp_FSI_nonOccludedKeyguard() { - Notification n = new Notification.Builder(getContext(), "a") - .setFullScreenIntent(mock(PendingIntent.class), true) - .build(); - NotificationEntry entry = new NotificationEntryBuilder() + private NotificationEntry createNotificationEntry() { + return new NotificationEntryBuilder() .setPkg("a") .setOpPkg("a") .setTag("a") - .setNotification(n) + .setNotification(new Notification.Builder(getContext(), "a").build()) .build(); - - when(mKeyguardStateController.isShowing()).thenReturn(true); - when(mKeyguardStateController.isOccluded()).thenReturn(false); - assertFalse(mInterruptSuppressor.suppressAwakeHeadsUp(entry)); } - @Test - public void testSuppressInterruptions_vrMode() { - Notification n = new Notification.Builder(getContext(), "a").build(); - NotificationEntry entry = new NotificationEntryBuilder() - .setPkg("a") - .setOpPkg("a") - .setTag("a") - .setNotification(n) + private NotificationEntry createFsiNotificationEntry() { + final Notification notification = new Notification.Builder(getContext(), "a") + .setFullScreenIntent(mock(PendingIntent.class), true) .build(); - mStatusBarNotificationPresenter.mVrMode = true; - - assertTrue("Vr mode should suppress interruptions", - mInterruptSuppressor.suppressAwakeInterruptions(entry)); - } - @Test - public void testSuppressInterruptions_statusBarAlertsDisabled() { - Notification n = new Notification.Builder(getContext(), "a").build(); - NotificationEntry entry = new NotificationEntryBuilder() + return new NotificationEntryBuilder() .setPkg("a") .setOpPkg("a") .setTag("a") - .setNotification(n) + .setNotification(notification) .build(); - when(mNotificationAlertsInteractor.areNotificationAlertsEnabled()).thenReturn(false); - - assertTrue("When alerts aren't enabled, interruptions are suppressed", - mInterruptSuppressor.suppressInterruptions(entry)); } } |