/*
 * Copyright (C) 2020 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.wmshell;

import static android.app.Notification.FLAG_BUBBLE;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.provider.Settings.Global.HEADS_UP_NOTIFICATIONS_ENABLED;
import static android.provider.Settings.Global.HEADS_UP_ON;
import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_DELETED;
import static android.service.notification.NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED;
import static android.service.notification.NotificationListenerService.REASON_APP_CANCEL;
import static android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED;
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_BANNED;
import static android.service.notification.NotificationListenerService.REASON_PACKAGE_CHANGED;

import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.server.notification.Flags.FLAG_SCREENSHARE_NOTIFICATION_HIDING;
import static com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_BAR;

import static com.google.common.truth.Truth.assertThat;

import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyList;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;

import android.app.ActivityManager;
import android.app.IActivityManager;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.hardware.display.AmbientDisplayConfiguration;
import android.os.Handler;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.FlagsParameterization;
import android.service.dreams.IDreamManager;
import android.service.notification.NotificationListenerService;
import android.service.notification.ZenModeConfig;
import android.testing.TestableLooper;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;
import android.view.Display;
import android.view.IWindowManager;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.WindowManager;

import androidx.annotation.Nullable;
import androidx.test.filters.SmallTest;

import com.android.internal.colorextraction.ColorExtractor;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.protolog.ProtoLog;
import com.android.internal.statusbar.IStatusBarService;
import com.android.launcher3.icons.BubbleIconFactory;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.annotations.SlowerThanOneSecond;
import com.android.systemui.biometrics.AuthController;
import com.android.systemui.colorextraction.SysuiColorExtractor;
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor;
import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FakeFeatureFlagsClassic;
import com.android.systemui.flags.SceneContainerFlagParameterizationKt;
import com.android.systemui.keyguard.KeyguardViewMediator;
import com.android.systemui.kosmos.KosmosJavaAdapter;
import com.android.systemui.model.SysUiState;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.scene.FakeWindowRootViewComponent;
import com.android.systemui.settings.UserTracker;
import com.android.systemui.shade.NotificationShadeWindowControllerImpl;
import com.android.systemui.shade.NotificationShadeWindowView;
import com.android.systemui.shade.ShadeController;
import com.android.systemui.shade.ShadeWindowLogger;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel;
import com.android.systemui.shared.notifications.domain.interactor.NotificationSettingsInteractor;
import com.android.systemui.shared.system.QuickStepContract;
import com.android.systemui.statusbar.NotificationEntryHelper;
import com.android.systemui.statusbar.NotificationLockscreenUserManager;
import com.android.systemui.statusbar.RankingBuilder;
import com.android.systemui.statusbar.SysuiStatusBarStateController;
import com.android.systemui.statusbar.notification.NotifPipelineFlags;
import com.android.systemui.statusbar.notification.collection.GroupEntry;
import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.UpdateSource;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.interruption.AvalancheProvider;
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionLogger;
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProvider;
import com.android.systemui.statusbar.notification.interruption.VisualInterruptionDecisionProviderTestUtil;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
import com.android.systemui.statusbar.phone.DozeParameters;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
import com.android.systemui.statusbar.policy.ZenModeController;
import com.android.systemui.statusbar.policy.data.repository.FakeDeviceProvisioningRepository;
import com.android.systemui.statusbar.policy.domain.interactor.DeviceProvisioningInteractor;
import com.android.systemui.user.domain.interactor.SelectedUserInteractor;
import com.android.systemui.util.FakeEventLog;
import com.android.systemui.util.settings.FakeGlobalSettings;
import com.android.systemui.util.settings.SystemSettings;
import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.Flags;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleData;
import com.android.wm.shell.bubbles.BubbleDataRepository;
import com.android.wm.shell.bubbles.BubbleEducationController;
import com.android.wm.shell.bubbles.BubbleEntry;
import com.android.wm.shell.bubbles.BubbleLogger;
import com.android.wm.shell.bubbles.BubbleOverflow;
import com.android.wm.shell.bubbles.BubbleResizabilityChecker;
import com.android.wm.shell.bubbles.BubbleStackView;
import com.android.wm.shell.bubbles.BubbleTaskView;
import com.android.wm.shell.bubbles.BubbleTransitions;
import com.android.wm.shell.bubbles.BubbleViewInfoTask;
import com.android.wm.shell.bubbles.BubbleViewProvider;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.bubbles.StackEducationView;
import com.android.wm.shell.bubbles.appinfo.PackageManagerBubbleAppInfoProvider;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.bubbles.logging.BubbleSessionTracker;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.HomeIntentProvider;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
import com.android.wm.shell.shared.bubbles.BubbleBarUpdate;
import com.android.wm.shell.sysui.ShellCommandHandler;
import com.android.wm.shell.sysui.ShellController;
import com.android.wm.shell.sysui.ShellInit;
import com.android.wm.shell.taskview.TaskView;
import com.android.wm.shell.taskview.TaskViewRepository;
import com.android.wm.shell.taskview.TaskViewTransitions;
import com.android.wm.shell.transition.Transitions;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.stubbing.Answer;

import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
import platform.test.runner.parameterized.Parameters;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;

@SmallTest
@RunWith(ParameterizedAndroidJunit4.class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class BubblesTest extends SysuiTestCase {

    private static final String TAG = "BubblesTest";

    @Mock
    private CommonNotifCollection mCommonNotifCollection;
    @Mock
    private BubblesManager.NotifCallback mNotifCallback;
    @Mock
    private WindowManager mWindowManager;
    @Mock
    private IActivityManager mActivityManager;
    @Mock
    private DozeParameters mDozeParameters;
    @Mock
    private ConfigurationController mConfigurationController;
    @Mock
    private ZenModeController mZenModeController;
    @Mock
    private ZenModeConfig mZenModeConfig;
    @Mock
    private NotificationLockscreenUserManager mLockscreenUserManager;
    @Mock
    private SysuiStatusBarStateController mStatusBarStateController;
    @Mock
    private KeyguardViewMediator mKeyguardViewMediator;
    @Mock
    private KeyguardBypassController mKeyguardBypassController;
    @Mock
    private BubbleDataRepository mDataRepository;
    @Mock
    private BubbleTransitions mBubbleTransitions;
    @Mock
    private NotificationShadeWindowView mNotificationShadeWindowView;
    @Mock
    private AuthController mAuthController;
    @Mock
    private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController;

    private SysUiState mSysUiState;
    private boolean mSysUiStateBubblesExpanded;
    private boolean mSysUiStateBubblesManageMenuExpanded;

    @Captor
    private ArgumentCaptor<NotifCollectionListener> mNotifListenerCaptor;
    @Captor
    private ArgumentCaptor<List<Bubble>> mBubbleListCaptor;
    @Captor
    private ArgumentCaptor<IntentFilter> mFilterArgumentCaptor;
    @Captor
    private ArgumentCaptor<BroadcastReceiver> mBroadcastReceiverArgumentCaptor;
    @Captor
    private ArgumentCaptor<KeyguardStateController.Callback> mKeyguardStateControllerCallbackCaptor;
    @Captor
    private ArgumentCaptor<Runnable> mSensitiveStateChangedListener;

    private BubblesManager mBubblesManager;
    private TestableBubbleController mBubbleController;
    private NotificationShadeWindowControllerImpl mNotificationShadeWindowController;
    private NotifCollectionListener mEntryListener;
    private NotificationEntry mEntry;
    private NotificationEntry mEntry2;
    private NotificationEntry mNonBubbleNotifEntry;
    private BubbleEntry mBubbleEntry;
    private BubbleEntry mBubbleEntry2;

    private BubbleEntry mBubbleEntryUser11;
    private BubbleEntry mBubbleEntry2User11;

    private Intent mNotesBubbleIntent;

    @Mock
    private ShellInit mShellInit;
    @Mock
    private ShellCommandHandler mShellCommandHandler;
    @Mock
    private ShellController mShellController;
    @Mock
    private Bubbles.BubbleExpandListener mBubbleExpandListener;
    @Mock
    private SysuiColorExtractor mColorExtractor;
    @Mock
    ColorExtractor.GradientColors mGradientColors;
    @Mock
    private ShadeController mShadeController;
    @Mock
    private NotifPipeline mNotifPipeline;
    @Mock
    private DumpManager mDumpManager;
    @Mock
    private IStatusBarService mStatusBarService;
    @Mock
    private IDreamManager mIDreamManager;
    @Mock
    private NotificationVisibilityProvider mVisibilityProvider;
    @Mock
    private LauncherApps mLauncherApps;
    @Mock
    private DisplayInsetsController mDisplayInsetsController;
    @Mock
    private DisplayImeController mDisplayImeController;
    @Mock
    private BubbleLogger mBubbleLogger;
    @Mock
    private BubbleSessionTracker mSessionTracker;
    @Mock
    private BubbleEducationController mEducationController;
    @Mock
    private TaskStackListenerImpl mTaskStackListener;
    @Mock
    private KeyguardStateController mKeyguardStateController;
    @Mock
    Transitions mTransitions;
    @Mock
    private Optional<OneHandedController> mOneHandedOptional;
    @Mock
    private UserManager mUserManager;
    @Mock
    private ShadeWindowLogger mShadeWindowLogger;
    @Mock
    private SelectedUserInteractor mSelectedUserInteractor;
    @Mock
    private UserTracker mUserTracker;
    @Mock
    private NotifPipelineFlags mNotifPipelineFlags;
    @Mock
    private Icon mNotesBubbleIcon;
    @Mock
    private Display mDefaultDisplay;
    @Mock
    private SyncTransactionQueue mSyncQueue;
    @Mock
    private HomeIntentProvider mHomeIntentProvider;

    private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
    private ShadeInteractor mShadeInteractor;
    private NotificationShadeWindowModel mNotificationShadeWindowModel;
    private ShellTaskOrganizer mShellTaskOrganizer;
    private TaskViewRepository mTaskViewRepository;
    private TaskViewTransitions mTaskViewTransitions;
    private PackageManagerBubbleAppInfoProvider mAppInfoProvider;

    private TestableBubblePositioner mPositioner;

    private BubbleData mBubbleData;

    private TestableLooper mTestableLooper;

    private final FakeFeatureFlagsClassic mFeatureFlags = new FakeFeatureFlagsClassic();

    private UserHandle mUser0;

    @Parameters(name = "{0}")
    public static List<FlagsParameterization> getParams() {
        return SceneContainerFlagParameterizationKt.parameterizeSceneContainerFlag();
    }

    public BubblesTest(FlagsParameterization flags) {
        mSetFlagsRule.setFlagsParameterization(flags);
    }

    @Before
    public void setUp() throws Exception {
        // Make sure ProtoLog is initialized before any logging occurs.
        ProtoLog.init();

        MockitoAnnotations.initMocks(this);
        PhysicsAnimatorTestUtils.prepareForTest();

        mTestableLooper = TestableLooper.get(this);

        // For the purposes of this test, just run everything synchronously
        ShellExecutor syncExecutor = new SyncExecutor();

        mUser0 = createUserHandle(/* userId= */ 0);

        when(mColorExtractor.getNeutralColors()).thenReturn(mGradientColors);
        when(mNotificationShadeWindowView.getViewTreeObserver())
                .thenReturn(mock(ViewTreeObserver.class));
        when(mWindowManager.getDefaultDisplay()).thenReturn(mDefaultDisplay);


        FakeDeviceProvisioningRepository deviceProvisioningRepository =
                mKosmos.getFakeDeviceProvisioningRepository();
        deviceProvisioningRepository.setDeviceProvisioned(true);

        DeviceEntryUdfpsInteractor deviceEntryUdfpsInteractor =
                mock(DeviceEntryUdfpsInteractor.class);
        when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false));

        mShadeInteractor = mKosmos.getShadeInteractor();
        mNotificationShadeWindowModel = mKosmos.getNotificationShadeWindowModel();

        mNotificationShadeWindowController = new NotificationShadeWindowControllerImpl(
                mContext,
                new FakeWindowRootViewComponent.Factory(mNotificationShadeWindowView),
                mWindowManager,
                mActivityManager,
                mDozeParameters,
                mStatusBarStateController,
                mConfigurationController,
                mKeyguardViewMediator,
                mKeyguardBypassController,
                syncExecutor,
                syncExecutor,
                mColorExtractor,
                mDumpManager,
                mKeyguardStateController,
                mAuthController,
                () -> mShadeInteractor,
                mShadeWindowLogger,
                () -> mSelectedUserInteractor,
                mUserTracker,
                mNotificationShadeWindowModel,
                mKosmos::getCommunalInteractor,
                mKosmos.getShadeLayoutParams(),
                mKosmos.getTopUiController()
        );
        mNotificationShadeWindowController.fetchWindowRootView();
        mNotificationShadeWindowController.attach();

        mNotesBubbleIntent = new Intent(mContext, BubblesTestActivity.class);
        mNotesBubbleIntent.setPackage(mContext.getPackageName());

        when(mZenModeController.getConfig()).thenReturn(mZenModeConfig);

        mSysUiState = mKosmos.getSysuiState();
        mSysUiState.addCallback((sysUiFlags, displayId) -> {
            mSysUiStateBubblesManageMenuExpanded =
                    (sysUiFlags
                            & QuickStepContract.SYSUI_STATE_BUBBLES_MANAGE_MENU_EXPANDED) != 0;
            mSysUiStateBubblesExpanded =
                    (sysUiFlags & QuickStepContract.SYSUI_STATE_BUBBLES_EXPANDED) != 0;
        });

        mPositioner = new TestableBubblePositioner(mContext,
                mContext.getSystemService(WindowManager.class));
        mPositioner.setMaxBubbles(5);
        mBubbleData = new BubbleData(mContext, mBubbleLogger, mPositioner, mEducationController,
                syncExecutor, syncExecutor);

        when(mUserManager.getProfiles(ActivityManager.getCurrentUser())).thenReturn(
                Collections.singletonList(mock(UserInfo.class)));

        final FakeGlobalSettings fakeGlobalSettings = new FakeGlobalSettings();
        fakeGlobalSettings.putInt(HEADS_UP_NOTIFICATIONS_ENABLED, HEADS_UP_ON);

        final VisualInterruptionDecisionProvider interruptionDecisionProvider =
                VisualInterruptionDecisionProviderTestUtil.INSTANCE.createProviderByFlag(
                        mock(AmbientDisplayConfiguration.class),
                        mock(BatteryController.class),
                        mock(DeviceProvisionedController.class),
                        new FakeEventLog(),
                        mock(NotifPipelineFlags.class),
                        fakeGlobalSettings,
                        mock(HeadsUpManager.class),
                        mock(KeyguardNotificationVisibilityProvider.class),
                        mock(KeyguardStateController.class),
                        mock(Handler.class),
                        mock(VisualInterruptionDecisionLogger.class),
                        mock(NotificationInterruptLogger.class),
                        mock(PowerManager.class),
                        mock(StatusBarStateController.class),
                        mock(SystemClock.class),
                        mock(UiEventLogger.class),
                        mock(UserTracker.class),
                        mock(AvalancheProvider.class),
                        mock(SystemSettings.class),
                        mock(PackageManager.class),
                        Optional.of(mock(Bubbles.class)),
                        mContext,
                        mock(NotificationManager.class),
                        mock(NotificationSettingsInteractor.class),
                        mock(DeviceProvisioningInteractor.class)
                );
        interruptionDecisionProvider.start();

        mShellTaskOrganizer = new ShellTaskOrganizer(mock(ShellInit.class),
                mock(ShellCommandHandler.class),
                mock(RootTaskDisplayAreaOrganizer.class),
                null,
                Optional.empty(),
                Optional.empty(),
                syncExecutor);
        mTaskViewRepository = new TaskViewRepository();
        mTaskViewTransitions = new TaskViewTransitions(mTransitions, mTaskViewRepository,
                mShellTaskOrganizer, mSyncQueue);
        mAppInfoProvider = new PackageManagerBubbleAppInfoProvider();
        mBubbleController = new TestableBubbleController(
                mContext,
                mShellInit,
                mShellCommandHandler,
                mShellController,
                mBubbleData,
                new FloatingContentCoordinator(),
                mDataRepository,
                mBubbleTransitions,
                mStatusBarService,
                mWindowManager,
                mDisplayInsetsController,
                mDisplayImeController,
                mUserManager,
                mLauncherApps,
                mBubbleLogger,
                mTaskStackListener,
                mShellTaskOrganizer,
                mPositioner,
                mock(DisplayController.class),
                mOneHandedOptional,
                mock(DragAndDropController.class),
                syncExecutor,
                mock(Handler.class),
                mTaskViewTransitions,
                mTransitions,
                mock(SyncTransactionQueue.class),
                mock(IWindowManager.class),
                new BubbleResizabilityChecker(),
                mHomeIntentProvider,
                mAppInfoProvider,
                Optional.empty(),
                mSessionTracker);
        mBubbleController.setExpandListener(mBubbleExpandListener);
        spyOn(mBubbleController);

        mBubblesManager = new BubblesManager(
                mContext,
                mBubbleController.asBubbles(),
                mNotificationShadeWindowController,
                mKosmos.getTopUiController(),
                mKeyguardStateController,
                mShadeController,
                mStatusBarService,
                mock(INotificationManager.class),
                mIDreamManager,
                mVisibilityProvider,
                interruptionDecisionProvider,
                mZenModeController,
                mLockscreenUserManager,
                mSensitiveNotificationProtectionController,
                mCommonNotifCollection,
                mNotifPipeline,
                mSysUiState,
                mFeatureFlags,
                mNotifPipelineFlags,
                syncExecutor,
                syncExecutor);
        mBubblesManager.addNotifCallback(mNotifCallback);

        // Need notifications for bubbles
        mEntry = mKosmos.createBubbledEntry(NotificationEntryBuilder::done);
        mEntry2 = mKosmos.createBubbledEntry(NotificationEntryBuilder::done);
        mNonBubbleNotifEntry = mKosmos.buildNotificationEntry(NotificationEntryBuilder::done);
        mBubbleEntry = mBubblesManager.notifToBubbleEntry(mEntry);
        mBubbleEntry2 = mBubblesManager.notifToBubbleEntry(mEntry2);

        UserHandle handle = UserHandle.of(11);
        mBubbleEntryUser11 = mBubblesManager.notifToBubbleEntry(
                mKosmos.createBubbledEntry(builder -> {
                    builder.setUser(handle);
                    return builder.done();
                }));
        mBubbleEntry2User11 = mBubblesManager.notifToBubbleEntry(
                mKosmos.createBubbledEntry(builder -> {
                    builder.setUser(handle);
                    return builder.done();
                }));

        // Get a reference to the BubbleController's entry listener
        verify(mNotifPipeline, atLeastOnce())
                .addCollectionListener(mNotifListenerCaptor.capture());
        mEntryListener = mNotifListenerCaptor.getValue();

        // Get a reference to KeyguardStateController.Callback
        verify(mKeyguardStateController, atLeastOnce())
                .addCallback(mKeyguardStateControllerCallbackCaptor.capture());

        // Make sure mocks are set up for current user
        switchUser(ActivityManager.getCurrentUser());
    }

    @After
    public void tearDown() throws Exception {
        if (mBubbleData != null) {
            ArrayList<Bubble> bubbles = new ArrayList<>(mBubbleData.getBubbles());
            for (int i = 0; i < bubbles.size(); i++) {
                mBubbleController.removeBubble(bubbles.get(i).getKey(),
                        Bubbles.DISMISS_NO_LONGER_BUBBLE);
            }
        }
        mTestableLooper.processAllMessages();

        // check that no animations are running before finishing the test to make sure that the
        // state gets cleaned up correctly between tests.
        int retryCount = 0;
        while (PhysicsAnimatorTestUtils.isAnyAnimationRunning() && retryCount <= 10) {
            Log.d(
                    TAG,
                    String.format("waiting for animations to complete. attempt %d", retryCount));
            // post a message to the looper and wait for it to be processed
            mTestableLooper.runWithLooper(() -> {
            });
            retryCount++;
        }
        mTestableLooper.processAllMessages();
        if (PhysicsAnimatorTestUtils.isAnyAnimationRunning()) {
            Log.d(TAG, "finished waiting for animations to complete but animations are still "
                    + "running");
        } else {
            Log.d(TAG, "no animations are running");
        }
    }

    @Test
    public void bubblesHiddenWhileDreaming() throws RemoteException {
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());
        assertThat(mBubbleController.getStackView().getVisibility()).isEqualTo(View.VISIBLE);

        when(mIDreamManager.isDreamingOrInPreview()).thenReturn(true); // dreaming is happening
        when(mKeyguardStateController.isShowing()).thenReturn(false); // device is unlocked
        KeyguardStateController.Callback callback =
                mKeyguardStateControllerCallbackCaptor.getValue();
        callback.onKeyguardShowingChanged();

        // Dreaming should hide bubbles
        assertThat(mBubbleController.getStackView().getVisibility()).isEqualTo(View.INVISIBLE);

        // Finish dreaming should show bubbles
        mNotificationShadeWindowController.setDreaming(false);
        when(mIDreamManager.isDreamingOrInPreview()).thenReturn(false); // dreaming finished

        // Dreaming updates come through mNotificationShadeWindowController
        mNotificationShadeWindowController.notifyStateChangedCallbacks();

        assertThat(mBubbleController.getStackView().getVisibility()).isEqualTo(View.VISIBLE);
    }

    @Test
    public void instantiateController_addInitCallback() {
        verify(mShellInit, times(1)).addInitCallback(any(), any());
    }

    @Test
    public void instantiateController_registerConfigChangeListener() {
        verify(mShellController, times(1)).addConfigurationChangeListener(any());
    }

    @Test
    public void instantiateController_registerTransitionObserver() {
        verify(mTransitions).registerObserver(any());
    }

    @Test
    public void testAddBubble() {
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());
        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testHasBubbles() {
        assertFalse(mBubbleController.hasBubbles());
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());
        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testRemoveBubble() {
        mBubbleController.updateBubble(mBubbleEntry);
        assertNotNull(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()));
        assertTrue(mBubbleController.hasBubbles());
        verify(mNotifCallback, times(1)).invalidateNotifications(anyString());

        mBubbleController.removeBubble(
                mEntry.getKey(), Bubbles.DISMISS_USER_GESTURE);
        assertNull(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()));
        verify(mNotifCallback, times(2)).invalidateNotifications(anyString());

        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testRemoveBubble_withDismissedNotif_inOverflow() {
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        // Make it look like dismissed notif
        mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).setSuppressNotification(true);

        // Now remove the bubble
        mBubbleController.removeBubble(
                mEntry.getKey(), Bubbles.DISMISS_USER_GESTURE);
        assertTrue(mBubbleData.hasOverflowBubbleWithKey(mEntry.getKey()));

        // We don't remove the notification since the bubble is still in overflow.
        verify(mNotifCallback, never()).removeNotification(eq(mEntry), any(), anyInt());
        assertFalse(mBubbleController.hasBubbles());
    }

    @Test
    public void testRemoveBubble_withDismissedNotif_notInOverflow() {
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);
        when(mCommonNotifCollection.getEntry(mEntry.getKey())).thenReturn(mEntry);

        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        // Make it look like dismissed notif
        mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).setSuppressNotification(true);

        // Now remove the bubble
        mBubbleController.removeBubble(
                mEntry.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
        assertFalse(mBubbleData.hasOverflowBubbleWithKey(mEntry.getKey()));

        // Since the notif is dismissed and not in overflow, once the bubble is removed,
        // removeNotification gets called to really remove the notif
        verify(mNotifCallback, times(1)).removeNotification(eq(mEntry),
                any(), anyInt());
        assertFalse(mBubbleController.hasBubbles());
    }

    @Test
    public void testDismissStack() {
        mBubbleController.updateBubble(mBubbleEntry);
        verify(mNotifCallback, times(1)).invalidateNotifications(anyString());
        assertNotNull(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()));
        mBubbleController.updateBubble(mBubbleEntry2);
        verify(mNotifCallback, times(2)).invalidateNotifications(anyString());
        assertNotNull(mBubbleData.getBubbleInStackWithKey(mEntry2.getKey()));
        assertTrue(mBubbleController.hasBubbles());

        mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
        verify(mNotifCallback, times(3)).invalidateNotifications(anyString());
        assertNull(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()));
        assertNull(mBubbleData.getBubbleInStackWithKey(mEntry2.getKey()));

        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testExpandCollapseStack() {
        assertStackCollapsed();

        // Mark it as a bubble and add it explicitly
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        // We should have bubbles & their notifs should not be suppressed
        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        // Expand the stack
        mBubbleData.setExpanded(true);
        assertStackExpanded();
        verify(mBubbleExpandListener).onBubbleExpandChanged(true, mEntry.getKey());
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);

        // Make sure the notif is suppressed
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);

        // Collapse
        mBubbleController.collapseStack();
        verify(mBubbleExpandListener).onBubbleExpandChanged(false, mEntry.getKey());
        assertStackCollapsed();
        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testCollapseAfterChangingExpandedBubble() {
        // Mark it as a bubble and add it explicitly
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryAdded(mEntry2);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.updateBubble(mBubbleEntry2);

        // We should have bubbles & their notifs should not be suppressed
        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry2);

        // Expand
        BubbleStackView stackView = mBubbleController.getStackView();
        mBubbleData.setExpanded(true);
        assertStackExpanded();
        verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
                true, mEntry2.getKey());
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);

        // Last added is the one that is expanded
        assertEquals(mEntry2.getKey(), mBubbleData.getSelectedBubble().getKey());
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry2);

        // Switch which bubble is expanded
        mBubbleData.setSelectedBubble(mBubbleData.getBubbleInStackWithKey(
                mEntry.getKey()));
        mBubbleData.setExpanded(true);
        assertEquals(mEntry.getKey(), mBubbleData.getBubbleInStackWithKey(
                stackView.getExpandedBubble().getKey()).getKey());
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);

        // collapse for previous bubble
        verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
                false, mEntry2.getKey());
        // expand for selected bubble
        verify(mBubbleExpandListener, atLeastOnce()).onBubbleExpandChanged(
                true, mEntry.getKey());


        // Collapse
        mBubbleController.collapseStack();
        assertStackCollapsed();
        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testExpansionRemovesShowInShadeAndDot() {
        // Mark it as a bubble and add it explicitly
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        // We should have bubbles & their notifs should not be suppressed
        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        mTestableLooper.processAllMessages();
        assertTrue(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showDot());

        // Expand
        mBubbleData.setExpanded(true);
        assertStackExpanded();
        verify(mBubbleExpandListener).onBubbleExpandChanged(true, mEntry.getKey());
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);

        // Notif is suppressed after expansion
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
        // Notif shouldn't show dot after expansion
        assertFalse(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showDot());
    }

    @Test
    public void testUpdateWhileExpanded_DoesntChangeShowInShadeAndDot() {
        // Mark it as a bubble and add it explicitly
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        // We should have bubbles & their notifs should not be suppressed
        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        mTestableLooper.processAllMessages();
        assertTrue(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showDot());

        // Expand
        mBubbleData.setExpanded(true);
        assertStackExpanded();
        verify(mBubbleExpandListener).onBubbleExpandChanged(true, mEntry.getKey());
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);

        // Notif is suppressed after expansion
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
        // Notif shouldn't show dot after expansion
        assertFalse(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showDot());

        // Send update
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.App);

        // Nothing should have changed
        // Notif is suppressed after expansion
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
        // Notif shouldn't show dot after expansion
        assertFalse(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showDot());
    }

    @Test
    public void testRemoveLastExpanded_collapses() {
        // Mark it as a bubble and add it explicitly
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryAdded(mEntry2);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.updateBubble(mBubbleEntry2);

        // Expand
        BubbleStackView stackView = mBubbleController.getStackView();
        mBubbleData.setExpanded(true);

        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);

        assertStackExpanded();
        verify(mBubbleExpandListener).onBubbleExpandChanged(true, mEntry2.getKey());

        // Last added is the one that is expanded
        assertEquals(mEntry2.getKey(), mBubbleData.getBubbleInStackWithKey(
                stackView.getExpandedBubble().getKey()).getKey());
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry2);

        // Dismiss currently expanded
        mBubbleController.removeBubble(
                mBubbleData.getBubbleInStackWithKey(
                        stackView.getExpandedBubble().getKey()).getKey(),
                Bubbles.DISMISS_USER_GESTURE);
        verify(mBubbleExpandListener).onBubbleExpandChanged(false, mEntry2.getKey());

        // Make sure first bubble is selected
        assertEquals(mEntry.getKey(), mBubbleData.getBubbleInStackWithKey(
                stackView.getExpandedBubble().getKey()).getKey());
        verify(mBubbleExpandListener).onBubbleExpandChanged(true, mEntry.getKey());

        // Dismiss that one
        mBubbleController.removeBubble(
                mBubbleData.getBubbleInStackWithKey(
                        stackView.getExpandedBubble().getKey()).getKey(),
                Bubbles.DISMISS_USER_GESTURE);

        // We should be collapsed
        verify(mBubbleExpandListener).onBubbleExpandChanged(false, mEntry.getKey());
        assertFalse(mBubbleController.hasBubbles());
        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testRemoveLastExpandedEmptyOverflow_collapses() {
        // Mark it as a bubble and add it explicitly
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        // Expand
        BubbleStackView stackView = mBubbleController.getStackView();
        mBubbleData.setExpanded(true);

        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
        assertStackExpanded();
        verify(mBubbleExpandListener).onBubbleExpandChanged(true, mEntry.getKey());

        // Block the bubble so it won't be in the overflow
        mBubbleController.removeBubble(
                mBubbleData.getBubbleInStackWithKey(
                        stackView.getExpandedBubble().getKey()).getKey(),
                Bubbles.DISMISS_BLOCKED);

        verify(mBubbleExpandListener).onBubbleExpandChanged(false, mEntry.getKey());

        // We should be collapsed
        verify(mBubbleExpandListener).onBubbleExpandChanged(false, mEntry.getKey());
        assertFalse(mBubbleController.hasBubbles());
        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }


    @Test
    public void testAutoExpand_fails_noFlag() {
        assertStackCollapsed();
        setMetadataFlags(mEntry,
                Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, false /* enableFlag */);

        // Add the auto expand bubble
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        // Expansion shouldn't change
        verify(mBubbleExpandListener, never()).onBubbleExpandChanged(false /* expanded */,
                mEntry.getKey());
        assertStackCollapsed();
        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testAutoExpand_succeeds_withFlag() {
        setMetadataFlags(mEntry,
                Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, true /* enableFlag */);

        // Add the auto expand bubble
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        // Expansion should change
        verify(mBubbleExpandListener).onBubbleExpandChanged(true /* expanded */,
                mEntry.getKey());
        assertStackExpanded();
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testSuppressNotif_onInitialNotif() {
        setMetadataFlags(mEntry,
                Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */);

        // Add the suppress notif bubble
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        // Notif should be suppressed because we were foreground
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
        // Dot + flyout is hidden because notif is suppressed
        assertFalse(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showDot());
        assertFalse(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showFlyout());
        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testSuppressNotif_onUpdateNotif() {
        mBubbleController.updateBubble(mBubbleEntry);

        // Should not be suppressed
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);
        // Should show dot
        assertTrue(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showDot());

        // Update to suppress notif
        setMetadataFlags(mEntry,
                Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, true /* enableFlag */);
        mBubbleController.updateBubble(mBubbleEntry);

        // Notif should be suppressed
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
        // Dot + flyout is hidden because notif is suppressed
        assertFalse(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showDot());
        assertFalse(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showFlyout());
        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testMarkNewNotificationAsShowInShade() {
        mEntryListener.onEntryAdded(mEntry);
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        mTestableLooper.processAllMessages();
        assertTrue(mBubbleData.getBubbleInStackWithKey(mEntry.getKey()).showDot());
    }

    @Test
    public void testAddNotif_notBubble() {
        mEntryListener.onEntryAdded(mNonBubbleNotifEntry);
        mEntryListener.onEntryUpdated(mNonBubbleNotifEntry, /* source= */ UpdateSource.App);

        assertThat(mBubbleController.hasBubbles()).isFalse();
    }

    @Test
    public void testDeleteIntent_removeBubble_aged() throws PendingIntent.CanceledException {
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.removeBubble(mEntry.getKey(), Bubbles.DISMISS_AGED);

        verify(mEntry.getSbn().getNotification().getBubbleMetadata().getDeleteIntent(), never())
                .send();
    }

    @Test
    public void testDeleteIntent_removeBubble_user() throws PendingIntent.CanceledException {
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.removeBubble(
                mEntry.getKey(), Bubbles.DISMISS_USER_GESTURE);
        verify(mEntry.getSbn().getNotification().getBubbleMetadata().getDeleteIntent(), times(1))
                .send();
    }

    @Test
    public void testDeleteIntent_dismissStack() throws PendingIntent.CanceledException {
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.updateBubble(mBubbleEntry2);
        mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
        verify(mEntry.getSbn().getNotification().getBubbleMetadata().getDeleteIntent(), times(1))
                .send();
        verify(mEntry2.getSbn().getNotification().getBubbleMetadata().getDeleteIntent(), times(1))
                .send();
    }

    @Test
    public void testRemoveBubble_noLongerBubbleAfterUpdate()
            throws PendingIntent.CanceledException {
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());

        mEntry.getSbn().getNotification().flags &= ~FLAG_BUBBLE;
        NotificationListenerService.Ranking ranking = new RankingBuilder(
                mEntry.getRanking()).setCanBubble(false).build();
        mEntry.setRanking(ranking);
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.App);

        assertFalse(mBubbleController.hasBubbles());
        verify(mEntry.getSbn().getNotification().getBubbleMetadata().getDeleteIntent(), never())
                .send();
    }

    @Test
    public void testRemoveBubble_entryListenerRemove() {
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        assertTrue(mBubbleController.hasBubbles());

        // Removes the notification
        mEntryListener.onEntryRemoved(mEntry, REASON_APP_CANCEL);
        assertFalse(mBubbleController.hasBubbles());
    }

    @Test
    public void testNotifsBanned_entryListenerRemove() {
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        assertTrue(mBubbleController.hasBubbles());

        // Removes the notification
        mEntryListener.onEntryRemoved(mEntry, REASON_PACKAGE_BANNED);
        assertFalse(mBubbleController.hasBubbles());
    }

    @Test
    public void testNotifsPackageChanged_entryListenerRemove() {
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        assertTrue(mBubbleController.hasBubbles());

        // Removes the notification
        mEntryListener.onEntryRemoved(mEntry, REASON_PACKAGE_CHANGED);
        assertFalse(mBubbleController.hasBubbles());
    }

    @Test
    public void removeBubble_intercepted() {
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        boolean intercepted = mBubblesManager.handleDismissalInterception(mEntry);

        // Intercept!
        assertTrue(intercepted);
        // Should update show in shade state
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
    }

    @Test
    public void removeBubble_dismissIntoOverflow_intercepted() {
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        // Dismiss the bubble
        mBubbleController.removeBubble(mEntry.getKey(), Bubbles.DISMISS_USER_GESTURE);
        assertFalse(mBubbleController.hasBubbles());

        // Dismiss the notification
        boolean intercepted = mBubblesManager.handleDismissalInterception(mEntry);

        // Intercept dismissal since bubble is going into overflow
        assertTrue(intercepted);
    }

    @Test
    public void removeBubble_notIntercepted() {
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        // Dismiss the bubble
        mBubbleController.removeBubble(mEntry.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
        assertFalse(mBubbleController.hasBubbles());

        // Dismiss the notification
        boolean intercepted = mBubblesManager.handleDismissalInterception(mEntry);

        // Not a bubble anymore so we don't intercept dismissal.
        assertFalse(intercepted);
    }

    @Test
    public void testNotifyShadeSuppressionChange_notificationDismiss() {
        mEntryListener.onEntryAdded(mEntry);

        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        mBubblesManager.handleDismissalInterception(mEntry);

        // Should update show in shade state
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);

        // Should notify delegate that shade state changed
        verify(mBubbleController).onBubbleMetadataFlagChanged(
                mBubbleData.getBubbleInStackWithKey(mEntry.getKey()));
    }

    @Test
    public void testNotifyShadeSuppressionChange_bubbleExpanded() {
        mEntryListener.onEntryAdded(mEntry);

        assertTrue(mBubbleController.hasBubbles());
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        mBubbleData.setExpanded(true);

        // Once a bubble is expanded the notif is suppressed
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);

        // Should notify delegate that shade state changed
        verify(mBubbleController).onBubbleMetadataFlagChanged(
                mBubbleData.getBubbleInStackWithKey(mEntry.getKey()));
    }

    @Test
    @EnableFlags(NotificationBundleUi.FLAG_NAME)
    public void testBubbleSummaryDismissal_suppressesSummaryAndBubbleFromShade() throws Exception {
        // GIVEN a group summary with a bubble child
        NotificationEntry summaryEntry = mKosmos.buildNotificationEntry(builder -> {
            builder.modifyNotification(mContext)
                    .setGroup("group")
                    .setGroupSummary(true);
            return builder.done();
        });
        GroupEntryBuilder groupEntry = new GroupEntryBuilder()
                .setSummary(summaryEntry);
        NotificationEntry groupedBubble = mKosmos.createBubbledEntry(builder -> {
            builder.modifyNotification(mContext).setGroup("group");
            builder.setParent(GroupEntry.ROOT_ENTRY);
            return builder.done();
        });
        groupEntry.addChild(groupedBubble);
        GroupEntry groupSummary = groupEntry.build();

        mEntryListener.onEntryAdded(groupedBubble);
        when(mCommonNotifCollection.getEntry(groupedBubble.getKey()))
                .thenReturn(groupedBubble);
        assertTrue(mBubbleData.hasBubbleInStackWithKey(groupedBubble.getKey()));

        // WHEN the summary is dismissed
        mBubblesManager.handleDismissalInterception(groupSummary.getSummary());

        // THEN the summary and bubbled child are suppressed from the shade
        assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                groupedBubble.getKey(),
                groupedBubble.getSbn().getGroupKey()));
        assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                groupedBubble.getKey(),
                groupedBubble.getSbn().getGroupKey()));
        assertTrue(mBubbleData.isSummarySuppressed(
                groupSummary.getSummary().getSbn().getGroupKey()));
    }

    @Test
    @DisableFlags(NotificationBundleUi.FLAG_NAME)
    public void testBubbleSummaryDismissal_suppressesSummaryAndBubbleFromShade_rows()
            throws Exception {
        // GIVEN a group summary with a bubble child
        NotificationEntry summaryEntry = mKosmos.buildNotificationEntry(builder -> {
           builder.modifyNotification(mContext)
                   .setGroup("group")
                   .setGroupSummary(true);
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
           return builder.done();
        });
        ExpandableNotificationRow groupSummary = mKosmos.createRow(summaryEntry);
        NotificationEntry entry = mKosmos.createBubbledEntry(builder -> {
           builder.modifyNotification(mContext).setGroup("groupId");
           builder.updateSbn(sbn -> {
               sbn.setGroup(mContext, "groupId");
           });
           return builder.done();
        });
        ExpandableNotificationRow groupedBubble = mKosmos.createRow(entry);
        groupSummary.addChildNotification(groupedBubble);

        mEntryListener.onEntryAdded(entry);
        when(mCommonNotifCollection.getEntry(entry.getKey())).thenReturn(entry);
        assertTrue(mBubbleData.hasBubbleInStackWithKey(entry.getKey()));

        // WHEN the summary is dismissed
        mBubblesManager.handleDismissalInterception(summaryEntry);

        // THEN the summary and bubbled child are suppressed from the shade
        assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                summaryEntry.getKey(),
                summaryEntry.getSbn().getGroupKey()));
        assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                entry.getKey(),
                entry.getSbn().getGroupKey()));
        assertTrue(mBubbleData.isSummarySuppressed(summaryEntry.getSbn().getGroupKey()));
    }

    @Test
    @EnableFlags(NotificationBundleUi.FLAG_NAME)
    public void testAppRemovesSummary_removesAllBubbleChildren() throws Exception {
        // GIVEN a group summary with a bubble child
        NotificationEntry summaryEntry = mKosmos.buildNotificationEntry(builder -> {
            builder.modifyNotification(mContext)
                    .setGroup("group")
                    .setGroupSummary(true);
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        ExpandableNotificationRow groupSummary = mKosmos.createRow(summaryEntry);
        NotificationEntry entry = mKosmos.createBubbledEntry(builder -> {
            builder.modifyNotification(mContext).setGroup("groupId");
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        ExpandableNotificationRow groupedBubble = mKosmos.createRow(entry);
        mEntryListener.onEntryAdded(entry);
        when(mCommonNotifCollection.getEntry(entry.getKey())).thenReturn(entry);
        assertTrue(mBubbleData.hasBubbleInStackWithKey(entry.getKey()));

        // GIVEN the summary is dismissed
        mBubblesManager.handleDismissalInterception(summaryEntry);

        // WHEN the summary is cancelled by the app
        mEntryListener.onEntryRemoved(summaryEntry, REASON_APP_CANCEL);

        // THEN the summary and its children are removed from bubble data
        assertFalse(mBubbleData.hasBubbleInStackWithKey(groupedBubble.getKey()));
        assertFalse(mBubbleData.isSummarySuppressed(
                summaryEntry.getSbn().getGroupKey()));
    }

    @Test
    @DisableFlags(NotificationBundleUi.FLAG_NAME)
    public void testAppRemovesSummary_removesAllBubbleChildren_rows() throws Exception {
        // GIVEN a group summary with a bubble child
        NotificationEntry summaryEntry = mKosmos.buildNotificationEntry(builder -> {
            builder.modifyNotification(mContext)
                    .setGroup("group")
                    .setGroupSummary(true);
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        ExpandableNotificationRow groupSummary = mKosmos.createRow(summaryEntry);
        NotificationEntry entry = mKosmos.createBubbledEntry(builder -> {
            builder.modifyNotification(mContext).setGroup("groupId");
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        ExpandableNotificationRow groupedBubble = mKosmos.createRow(entry);

        mEntryListener.onEntryAdded(entry);
        when(mCommonNotifCollection.getEntry(entry.getKey())).thenReturn(entry);
        groupSummary.addChildNotification(groupedBubble);
        assertTrue(mBubbleData.hasBubbleInStackWithKey(entry.getKey()));

        // GIVEN the summary is dismissed
        mBubblesManager.handleDismissalInterception(summaryEntry);

        // WHEN the summary is cancelled by the app
        mEntryListener.onEntryRemoved(summaryEntry, REASON_APP_CANCEL);

        // THEN the summary and its children are removed from bubble data
        assertFalse(mBubbleData.hasBubbleInStackWithKey(summaryEntry.getKey()));
        assertFalse(mBubbleData.isSummarySuppressed(summaryEntry.getSbn().getGroupKey()));
    }

    @Test
    @EnableFlags(NotificationBundleUi.FLAG_NAME)
    public void testSummaryDismissalMarksBubblesHiddenFromShadeAndDismissesNonBubbledChildren()
            throws Exception {
        // GIVEN a group summary with two (non-bubble) children and one bubble child
        NotificationEntry summaryEntry = mKosmos.buildNotificationEntry(builder -> {
            builder.modifyNotification(mContext)
                    .setGroup("group")
                    .setGroupSummary(true);
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        GroupEntryBuilder groupEntry = new GroupEntryBuilder()
                .setSummary(summaryEntry);
        NotificationEntry groupedBubble = mKosmos.createBubbledEntry(builder -> {
            builder.modifyNotification(mContext).setGroup("group");
            builder.setParent(GroupEntry.ROOT_ENTRY);
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        // and two non-bubble children
        NotificationEntry child1 = mKosmos.buildNotificationEntry(builder -> {
            builder.modifyNotification(mContext).setGroup("groupId");
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            builder.setParent(GroupEntry.ROOT_ENTRY);
            return builder.done();
        });
        NotificationEntry child2 = mKosmos.buildNotificationEntry(builder -> {
            builder.modifyNotification(mContext).setGroup("groupId");
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            builder.setParent(GroupEntry.ROOT_ENTRY);
            return builder.done();
        });

        groupEntry.addChild(child1);
        groupEntry.addChild(child2);
        groupEntry.addChild(groupedBubble);
        GroupEntry groupSummary = groupEntry.build();

        mEntryListener.onEntryAdded(groupedBubble);
        when(mCommonNotifCollection.getEntry(groupedBubble.getKey())).thenReturn(groupedBubble);

        // WHEN the summary is dismissed
        mBubblesManager.handleDismissalInterception(summaryEntry);

        // THEN only the NON-bubble children are dismissed
        verify(mNotifCallback, times(1)).removeNotification(
                eq(child1), any(), eq(REASON_GROUP_SUMMARY_CANCELED));
        verify(mNotifCallback, times(1)).removeNotification(
                eq(child2), any(), eq(REASON_GROUP_SUMMARY_CANCELED));
        verify(mNotifCallback, never()).removeNotification(eq(groupedBubble), any(), anyInt());

        // THEN the bubble child still exists as a bubble and is suppressed from the shade
        assertTrue(mBubbleData.hasBubbleInStackWithKey(groupedBubble.getKey()));
        assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                groupedBubble.getKey(),
                groupedBubble.getSbn().getGroupKey()));
        assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                groupedBubble.getKey(),
                groupedBubble.getSbn().getGroupKey()));

        // THEN the summary is also suppressed from the shade
        assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                summaryEntry.getKey(),
                summaryEntry.getSbn().getGroupKey()));
        assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                summaryEntry.getKey(),
                summaryEntry.getSbn().getGroupKey()));
    }

    @Test
    @DisableFlags(NotificationBundleUi.FLAG_NAME)
    public void testSummaryDismissalMarksBubblesHiddenFromShadeAndDismissesNonBubbledChildren_row()
            throws Exception {
        // GIVEN a group summary with two (non-bubble) children and one bubble child
        NotificationEntry summaryEntry = mKosmos.buildNotificationEntry(builder -> {
            builder.modifyNotification(mContext)
                    .setGroup("group")
                    .setGroupSummary(true);
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        ExpandableNotificationRow groupSummary = mKosmos.createRow(summaryEntry);
        NotificationEntry entry = mKosmos.createBubbledEntry(builder -> {
            builder.modifyNotification(mContext).setGroup("groupId");
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        ExpandableNotificationRow groupedBubble = mKosmos.createRow(entry);
        groupSummary.addChildNotification(groupedBubble);
        // and two non-bubble children
        NotificationEntry child1 = mKosmos.buildNotificationEntry(builder -> {
            builder.modifyNotification(mContext).setGroup("groupId");
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        groupSummary.addChildNotification(mKosmos.createRow(child1));
        NotificationEntry child2 = mKosmos.buildNotificationEntry(builder -> {
            builder.modifyNotification(mContext).setGroup("groupId");
            builder.updateSbn(sbn -> {
                sbn.setGroup(mContext, "groupId");
            });
            return builder.done();
        });
        groupSummary.addChildNotification(mKosmos.createRow(child2));

        mEntryListener.onEntryAdded(entry);
        when(mCommonNotifCollection.getEntry(entry.getKey())).thenReturn(entry);

        // WHEN the summary is dismissed
        mBubblesManager.handleDismissalInterception(summaryEntry);

        // THEN only the NON-bubble children are dismissed
        List<ExpandableNotificationRow> childrenRows = groupSummary.getAttachedChildren();
        verify(mNotifCallback, times(1)).removeNotification(
                eq(child1), any(), eq(REASON_GROUP_SUMMARY_CANCELED));
        verify(mNotifCallback, times(1)).removeNotification(
                eq(child2), any(), eq(REASON_GROUP_SUMMARY_CANCELED));
        verify(mNotifCallback, never()).removeNotification(eq(entry), any(), anyInt());

        // THEN the bubble child still exists as a bubble and is suppressed from the shade
        assertTrue(mBubbleData.hasBubbleInStackWithKey(entry.getKey()));
        assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                entry.getKey(), entry.getSbn().getGroupKey()));
        assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                entry.getKey(), entry.getSbn().getGroupKey()));

        // THEN the summary is also suppressed from the shade
        assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                summaryEntry.getKey(),
                summaryEntry.getSbn().getGroupKey()));
        assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                summaryEntry.getKey(),
                summaryEntry.getSbn().getGroupKey()));
    }


    /**
     * Verifies that when the user changes, the bubbles in the overflow list is cleared. Doesn't
     * test the loading from the repository which would be a nice thing to add.
     */
    @SlowerThanOneSecond
    @Test
    public void testOnUserChanged_overflowState() {
        int firstUserId = mBubbleEntry.getStatusBarNotification().getUser().getIdentifier();
        int secondUserId = mBubbleEntryUser11.getStatusBarNotification().getUser().getIdentifier();

        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.updateBubble(mBubbleEntry2);
        assertTrue(mBubbleController.hasBubbles());
        mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);

        // Verify these are in the overflow
        assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey())).isNotNull();
        assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2.getKey())).isNotNull();

        // Switch users
        switchUser(secondUserId);
        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();

        // Give this user some bubbles
        mBubbleController.updateBubble(mBubbleEntryUser11);
        mBubbleController.updateBubble(mBubbleEntry2User11);
        assertTrue(mBubbleController.hasBubbles());
        mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);

        // Verify these are in the overflow
        assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntryUser11.getKey())).isNotNull();
        assertThat(mBubbleData.getOverflowBubbleWithKey(mBubbleEntry2User11.getKey())).isNotNull();

        // Would have loaded bubbles twice because of user switch
        verify(mDataRepository, times(2)).loadBubbles(anyInt(), anyList(), any());
    }

    @Test
    public void testOnUserChanged_bubblesRestored() {
        int firstUserId = mBubbleEntry.getStatusBarNotification().getUser().getIdentifier();
        int secondUserId = mBubbleEntryUser11.getStatusBarNotification().getUser().getIdentifier();
        // Mock current profile
        when(mLockscreenUserManager.isCurrentProfile(firstUserId)).thenReturn(true);
        when(mLockscreenUserManager.isCurrentProfile(secondUserId)).thenReturn(false);

        mBubbleController.updateBubble(mBubbleEntry);
        assertThat(mBubbleController.hasBubbles()).isTrue();
        // We start with 1 bubble
        assertThat(mBubbleData.getBubbles()).hasSize(1);

        // Switch to second user
        switchUser(secondUserId);

        // Second user has no bubbles
        assertThat(mBubbleController.hasBubbles()).isFalse();

        // Send bubble update for first user, ensure it does not show up
        mBubbleController.updateBubble(mBubbleEntry2);
        assertThat(mBubbleController.hasBubbles()).isFalse();

        // Start returning notif for first user again
        when(mCommonNotifCollection.getAllNotifs()).thenReturn(Arrays.asList(mEntry, mEntry2));

        // Switch back to first user
        switchUser(firstUserId);

        // Check we now have two bubbles, one previous and one new that came in
        assertThat(mBubbleController.hasBubbles()).isTrue();
        // Now there are 2 bubbles
        assertThat(mBubbleData.getBubbles()).hasSize(2);
    }

    /**
     * Verifies we only load the overflow data once.
     */
    @Test
    public void testOverflowLoadedOnce() {
        // XXX
        when(mCommonNotifCollection.getEntry(mEntry.getKey())).thenReturn(mEntry);
        when(mCommonNotifCollection.getEntry(mEntry2.getKey())).thenReturn(mEntry2);

        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryAdded(mEntry2);
        mBubbleData.dismissAll(Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();

        mEntryListener.onEntryRemoved(mEntry, REASON_APP_CANCEL);
        mEntryListener.onEntryRemoved(mEntry2, REASON_APP_CANCEL);
        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();

        verify(mDataRepository, times(1)).loadBubbles(anyInt(), anyList(), any());
    }

    /**
     * Verifies that shortcut deletions triggers that bubble being removed from XML.
     */
    @Test
    public void testDeleteShortcutsDeletesXml() throws Exception {
        NotificationEntry entry = mKosmos.createShortcutBubbledEntry(
                NotificationEntryBuilder::done);
        BubbleEntry shortcutBubbleEntry = mBubblesManager.notifToBubbleEntry(entry);
        mBubbleController.updateBubble(shortcutBubbleEntry);

        mBubbleData.dismissBubbleWithKey(shortcutBubbleEntry.getKey(),
                Bubbles.DISMISS_SHORTCUT_REMOVED);

        verify(mDataRepository, atLeastOnce()).removeBubbles(anyInt(), mBubbleListCaptor.capture());
        assertThat(mBubbleListCaptor.getValue().get(0).getKey()).isEqualTo(
                shortcutBubbleEntry.getKey());
    }

    /**
     * Verifies that the package manager for the user is used when loading info for the bubble.
     */
    @Test
    public void test_bubbleViewInfoGetPackageForUser() throws Exception {
        final int workProfileUserId = 10;
        final UserHandle workUser = new UserHandle(workProfileUserId);
        final String workPkg = "work.pkg";

        final Bubble bubble = createBubble(workProfileUserId, workPkg);
        assertEquals(workProfileUserId, bubble.getUser().getIdentifier());

        final Context context = setUpContextWithPackageManager(workPkg, null /* AppInfo */);
        when(context.getResources()).thenReturn(mContext.getResources());
        final Context userContext = setUpContextWithPackageManager(workPkg,
                mock(ApplicationInfo.class));

        // If things are working correctly, CentralSurfaces.getPackageManagerForUser will call this
        when(context.createPackageContextAsUser(eq(workPkg), anyInt(), eq(workUser)))
                .thenReturn(userContext);

        BubbleViewInfoTask.BubbleViewInfo info = BubbleViewInfoTask.BubbleViewInfo.populate(context,
                () -> new BubbleTaskView(mock(TaskView.class), mock(Executor.class)),
                mPositioner,
                mBubbleController.getStackView(),
                new BubbleIconFactory(mContext,
                        mContext.getResources().getDimensionPixelSize(
                                com.android.wm.shell.R.dimen.bubble_size),
                        mContext.getResources().getDimensionPixelSize(
                                com.android.wm.shell.R.dimen.bubble_badge_size),
                        mContext.getResources().getColor(
                                com.android.launcher3.icons.R.color.important_conversation),
                        mContext.getResources().getDimensionPixelSize(
                                com.android.internal.R.dimen.importance_ring_stroke_width)),
                bubble,
                mAppInfoProvider,
                true /* skipInflation */);
        verify(userContext, times(1)).getPackageManager();
        verify(context, times(1)).createPackageContextAsUser(eq(workPkg),
                eq(Context.CONTEXT_RESTRICTED),
                eq(workUser));
        assertNotNull(info);
    }

    @Test
    public void testShowManageMenuChangesSysuiState() {
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());

        // Expand the stack
        BubbleStackView stackView = mBubbleController.getStackView();
        mBubbleData.setExpanded(true);
        assertStackExpanded();
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);

        // Show the menu
        stackView.showManageMenu(true);
        assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */);
        assertTrue(stackView.isManageMenuSettingsVisible());
        assertTrue(stackView.isManageMenuDontBubbleVisible());
    }

    @Test
    public void testShowManageMenuChangesSysuiState_notesBubble() {
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
        assertTrue(mBubbleController.hasBubbles());

        // Expand the stack
        BubbleStackView stackView = mBubbleController.getStackView();
        mBubbleData.setExpanded(true);
        assertStackExpanded();
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);

        // Show the menu
        stackView.showManageMenu(true);
        assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */);
        assertFalse(stackView.isManageMenuSettingsVisible());
        assertFalse(stackView.isManageMenuDontBubbleVisible());
    }

    @Test
    public void testHideManageMenuChangesSysuiState() {
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());

        // Expand the stack
        BubbleStackView stackView = mBubbleController.getStackView();
        mBubbleData.setExpanded(true);
        assertStackExpanded();
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);

        // Show the menu
        stackView.showManageMenu(true);
        assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */);

        // Hide the menu
        stackView.showManageMenu(false);
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testCollapseBubbleManageMenuChangesSysuiState() {
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());

        // Expand the stack
        BubbleStackView stackView = mBubbleController.getStackView();
        mBubbleData.setExpanded(true);
        assertStackExpanded();
        assertSysuiStates(true /* stackExpanded */, false /* manageMenuExpanded */);

        // Show the menu
        stackView.showManageMenu(true);
        assertSysuiStates(true /* stackExpanded */, true /* manageMenuExpanded */);

        // Collapse the stack
        mBubbleData.setExpanded(false);

        assertSysuiStates(false /* stackExpanded */, false /* manageMenuExpanded */);
    }

    @Test
    public void testNotificationChannelModified_channelUpdated_removesOverflowBubble()
            throws Exception {
        // Setup
        NotificationEntry entry = mKosmos.createShortcutBubbledEntry(
                NotificationEntryBuilder::done);
        mBubbleController.updateBubble(mBubblesManager.notifToBubbleEntry(entry));
        assertTrue(mBubbleController.hasBubbles());

        // Overflow it
        mBubbleData.dismissBubbleWithKey(entry.getKey(),
                Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.hasOverflowBubbleWithKey(entry.getKey())).isTrue();

        // Test
        entry.getChannel().setDeleted(true);
        mBubbleController.onNotificationChannelModified(entry.getSbn().getPackageName(),
                entry.getSbn().getUser(),
                entry.getChannel(),
                NOTIFICATION_CHANNEL_OR_GROUP_UPDATED);
        assertThat(mBubbleData.hasOverflowBubbleWithKey(entry.getKey())).isFalse();
    }

    @Test
    public void testNotificationChannelModified_channelDeleted_removesOverflowBubble()
            throws Exception {
        // Setup
        NotificationEntry entry = mKosmos.createShortcutBubbledEntry(
                NotificationEntryBuilder::done);
        mBubbleController.updateBubble(mBubblesManager.notifToBubbleEntry(entry));

        // Overflow it
        mBubbleData.dismissBubbleWithKey(entry.getKey(),
                Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.hasOverflowBubbleWithKey(entry.getKey())).isTrue();

        // Test
        entry.getChannel().setDeleted(true);
        mBubbleController.onNotificationChannelModified(entry.getSbn().getPackageName(),
                entry.getSbn().getUser(),
                entry.getChannel(),
                NOTIFICATION_CHANNEL_OR_GROUP_DELETED);
        assertThat(mBubbleData.hasOverflowBubbleWithKey(entry.getKey())).isFalse();
    }

    @Test
    public void testStackViewOnBackPressed_updatesBubbleDataExpandState() {
        mBubbleController.updateBubble(mBubbleEntry);

        // Expand the stack
        mBubbleData.setExpanded(true);
        assertStackExpanded();

        // Hit back
        BubbleStackView stackView = mBubbleController.getStackView();
        stackView.onBackPressed();

        // Make sure we're collapsed
        assertStackCollapsed();
    }


    @Test
    public void testRegisterUnregisterBroadcastListener() {
        spyOn(mContext);
        mBubbleController.updateBubble(mBubbleEntry);
        verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
        assertThat(mFilterArgumentCaptor.getValue()
                .hasAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)).isTrue();
        assertThat(mFilterArgumentCaptor.getValue()
                .hasAction(Intent.ACTION_SCREEN_OFF)).isTrue();

        mBubbleData.dismissBubbleWithKey(mBubbleEntry.getKey(), REASON_APP_CANCEL);
        // TODO: not certain why this isn't called normally when tests are run, perhaps because
        // it's after an animation in BSV. This calls BubbleController#removeFromWindowManagerMaybe
        mBubbleController.onAllBubblesAnimatedOut();

        verify(mContext).unregisterReceiver(eq(mBroadcastReceiverArgumentCaptor.getValue()));
    }

    @Test
    public void testBroadcastReceiverCloseDialogs_notGestureNav() {
        spyOn(mContext);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleData.setExpanded(true);
        verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
        Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);

        assertStackExpanded();
    }

    @Test
    public void testBroadcastReceiverCloseDialogs_reasonGestureNav() {
        spyOn(mContext);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleData.setExpanded(true);

        verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
        Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        i.putExtra("reason", "gestureNav");
        mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
        assertStackCollapsed();
    }

    @Test
    public void testBroadcastReceiverCloseDialogs_reasonHomeKey() {
        spyOn(mContext);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleData.setExpanded(true);

        verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
        Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        i.putExtra("reason", "homekey");
        mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
        assertStackCollapsed();
    }

    @Test
    public void testBroadcastReceiverCloseDialogs_reasonRecentsKey() {
        spyOn(mContext);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleData.setExpanded(true);

        verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));
        Intent i = new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
        i.putExtra("reason", "recentapps");
        mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
        assertStackCollapsed();
    }

    @Test
    public void testBroadcastReceiver_screenOff() {
        spyOn(mContext);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleData.setExpanded(true);

        verify(mContext).registerReceiver(mBroadcastReceiverArgumentCaptor.capture(),
                mFilterArgumentCaptor.capture(), eq(Context.RECEIVER_EXPORTED));

        Intent i = new Intent(Intent.ACTION_SCREEN_OFF);
        mBroadcastReceiverArgumentCaptor.getValue().onReceive(mContext, i);
        assertStackCollapsed();
    }

    @Test
    public void testOnStatusBarStateChanged() {
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleData.setExpanded(true);
        assertStackExpanded();
        BubbleStackView stackView = mBubbleController.getStackView();
        assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE);

        mBubbleController.onStatusBarStateChanged(false);

        assertStackCollapsed();
        assertThat(stackView.getVisibility()).isEqualTo(View.INVISIBLE);

        mBubbleController.onStatusBarStateChanged(true);
        assertThat(stackView.getVisibility()).isEqualTo(View.VISIBLE);
    }

    /**
     * Test to verify behavior for following situation:
     * <ul>
     *     <li>status bar shade state is set to <code>false</code></li>
     *     <li>there is a bubble pending to be expanded</li>
     * </ul>
     * Test that duplicate status bar state updates to <code>false</code> do not clear the
     * pending bubble to be
     * expanded.
     */
    @Test
    public void testOnStatusBarStateChanged_statusBarChangeDoesNotClearExpandingBubble() {
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.onStatusBarStateChanged(false);
        // Set the bubble to expand once status bar state changes
        mBubbleController.expandStackAndSelectBubble(mBubbleEntry);
        // Check that stack is currently collapsed
        assertStackCollapsed();
        // Post status bar state change update with the same value
        mBubbleController.onStatusBarStateChanged(false);
        // Stack should remain collapsed
        assertStackCollapsed();
        // Post status bar state change which should trigger bubble to expand
        mBubbleController.onStatusBarStateChanged(true);
        assertStackExpanded();
    }

    /**
     * Test to verify behavior for the following scenario:
     * <ol>
     *     <li>device is locked with keyguard on, status bar shade state updates to
     *     <code>false</code></li>
     *     <li>notification entry is marked to be a bubble and it is set to auto-expand</li>
     *     <li>device unlock starts, status bar shade state receives another update to
     *     <code>false</code></li>
     *     <li>device is unlocked and status bar shade state is set to <code>true</code></li>
     *     <li>bubble should be expanded</li>
     * </ol>
     */
    @Test
    public void testOnStatusBarStateChanged_newAutoExpandedBubbleRemainsExpanded() {
        // Set device as locked
        mBubbleController.onStatusBarStateChanged(false);

        // Create a auto-expanded bubble
        NotificationEntry entry = mKosmos.createBubbledEntry(builder -> {
            builder.modifyNotification(mContext)
                    .setBubbleMetadata(new Notification.BubbleMetadata.Builder(
                            getMetadata().getIntent(),
                            getMetadata().getIcon())
                            .setDesiredHeight(getMetadata().getDesiredHeight())
                            .setDesiredHeightResId(getMetadata().getDesiredHeightResId())
                            .setAutoExpandBubble(true)
                            .build());
            return builder.done();
        });
        mEntryListener.onEntryAdded(entry);

        // When unlocking, we may receive duplicate updates with shade=false, ensure they don't
        // clear the expanded state
        mBubbleController.onStatusBarStateChanged(false);
        mBubbleController.onStatusBarStateChanged(true);

        // After unlocking, stack should be expanded
        assertStackExpanded();
    }

    @Test
    public void testSetShouldAutoExpand_notifiesFlagChanged() {
        mBubbleController.updateBubble(mBubbleEntry);

        assertTrue(mBubbleController.hasBubbles());
        Bubble b = mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey());
        assertThat(b.shouldAutoExpand()).isFalse();

        // Set it to the same thing
        b.setShouldAutoExpand(false);

        // Verify it doesn't notify
        verify(mBubbleController, never()).onBubbleMetadataFlagChanged(any());

        // Set it to something different
        b.setShouldAutoExpand(true);
        verify(mBubbleController).onBubbleMetadataFlagChanged(b);
    }

    @Test
    public void testUpdateBubble_skipsDndSuppressListNotifs() {
        mBubbleEntry = new BubbleEntry(mEntry.getSbn(), mEntry.getRanking(), true, /* isDismissable */
                mEntry.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
                mEntry.shouldSuppressPeek());
        mBubbleEntry.getBubbleMetadata().setFlags(
                Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);

        mBubbleController.updateBubble(mBubbleEntry);

        Bubble b = mBubbleData.getPendingBubbleWithKey(mBubbleEntry.getKey());
        assertThat(b.shouldAutoExpand()).isFalse();
        assertThat(mBubbleData.getBubbleInStackWithKey(mBubbleEntry.getKey())).isNull();
    }

    @Test
    public void testOnRankingUpdate_DndSuppressListNotif() {
        // It's in the stack
        mBubbleController.updateBubble(mBubbleEntry);
        assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isTrue();

        // Set current user profile
        SparseArray<UserInfo> userInfos = new SparseArray<>();
        userInfos.put(mBubbleEntry.getStatusBarNotification().getUser().getIdentifier(),
                mock(UserInfo.class));
        mBubbleController.onCurrentProfilesChanged(userInfos);

        // Send ranking update that the notif is suppressed from the list.
        HashMap<String, Pair<BubbleEntry, Boolean>> entryDataByKey = new HashMap<>();
        mBubbleEntry = new BubbleEntry(mEntry.getSbn(), mEntry.getRanking(), true /* isDismissable */,
                mEntry.shouldSuppressNotificationDot(), true /* DndSuppressNotifFromList */,
                mEntry.shouldSuppressPeek());
        Pair<BubbleEntry, Boolean> pair = new Pair(mBubbleEntry, true);
        entryDataByKey.put(mBubbleEntry.getKey(), pair);

        NotificationListenerService.RankingMap rankingMap =
                mock(NotificationListenerService.RankingMap.class);
        when(rankingMap.getOrderedKeys()).thenReturn(new String[]{mBubbleEntry.getKey()});
        mBubbleController.onRankingUpdated(rankingMap, entryDataByKey);

        // Should no longer be in the stack
        assertThat(mBubbleData.hasBubbleInStackWithKey(mBubbleEntry.getKey())).isFalse();
    }

    /**
     * Verifies that if a bubble is in the overflow and a non-interruptive notification update
     * comes in for it, it stays in the overflow but the entry is updated.
     */
    @Test
    public void testNonInterruptiveUpdate_doesntBubbleFromOverflow() {
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.App);
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        // Dismiss the bubble so it's in the overflow
        mBubbleController.removeBubble(
                mEntry.getKey(), Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.hasOverflowBubbleWithKey(mEntry.getKey())).isTrue();

        // Update the entry to not show in shade
        setMetadataFlags(mEntry,
                Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, /* enableFlag= */ true);
        mBubbleController.updateBubble(mBubbleEntry,
                /* suppressFlyout= */ false, /* showInShade= */ true);

        // Check that the update was applied - shouldn't be show in shade
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
        // Check that it wasn't inflated (1 because it would've been inflated via onEntryAdded)
        verify(mBubbleController, times(1)).inflateAndAdd(
                any(Bubble.class), anyBoolean(), anyBoolean());
    }

    /**
     * Verifies that if a bubble is active, and a non-interruptive notification update comes in for
     * it, it doesn't trigger a new inflate and add for that bubble.
     */
    @Test
    public void testNonInterruptiveUpdate_doesntTriggerInflate() {
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.App);
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        // Update the entry to not show in shade
        setMetadataFlags(mEntry,
                Notification.BubbleMetadata.FLAG_SUPPRESS_NOTIFICATION, /* enableFlag= */ true);
        mBubbleController.updateBubble(mBubbleEntry,
                /* suppressFlyout= */ false, /* showInShade= */ true);

        // Check that the update was applied - shouldn't be show in shade
        assertBubbleNotificationSuppressedFromShade(mBubbleEntry);
        // Check that it wasn't inflated (1 because it would've been inflated via onEntryAdded)
        verify(mBubbleController, times(1)).inflateAndAdd(
                any(Bubble.class), anyBoolean(), anyBoolean());
    }

    /**
     * Verifies that if a bubble is in the overflow and a non-interruptive notification update
     * comes in for it with FLAG_BUBBLE that the flag is removed.
     */
    @Test
    public void testNonInterruptiveUpdate_doesntOverrideOverflowFlagBubble() {
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.App);
        assertBubbleNotificationNotSuppressedFromShade(mBubbleEntry);

        // Dismiss the bubble so it's in the overflow
        mBubbleController.removeBubble(
                mEntry.getKey(), Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.hasOverflowBubbleWithKey(mEntry.getKey())).isTrue();
        // Once it's in the overflow it's not actively a bubble (doesn't have FLAG_BUBBLE)
        Bubble b = mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey());
        assertThat(b.isBubble()).isFalse();

        // Send a non-notifying update that has FLAG_BUBBLE
        mEntry.getSbn().getNotification().flags = FLAG_BUBBLE;
        assertThat(mEntry.getSbn().getNotification().isBubbleNotification()).isTrue();
        mBubbleController.updateBubble(mBubbleEntry,
                /* suppressFlyout= */ false, /* showInShade= */ true);

        // Verify that it still doesn't have FLAG_BUBBLE because it's in the overflow.
        b = mBubbleData.getOverflowBubbleWithKey(mBubbleEntry.getKey());
        assertThat(b.isBubble()).isFalse();
    }

    @Test
    public void testNonSystemUpdatesIgnored() {
        mEntryListener.onEntryAdded(mEntry);
        assertThat(mBubbleController.hasBubbles()).isTrue();

        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.SystemUi);
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.SystemUi);
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.SystemUi);

        // Check that it wasn't inflated (1 because it would've been inflated via onEntryAdded)
        verify(mBubbleController, times(1)).inflateAndAdd(
                any(Bubble.class), anyBoolean(), anyBoolean());
    }

    @Test
    public void testShowStackEdu_isNotConversationBubble() {
        // Setup
        setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false);
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
        assertTrue(mBubbleController.hasBubbles());
        String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(mNotesBubbleIntent.getPackage(),
                mUser0);
        // Click on bubble
        Bubble bubble = mBubbleData.getBubbleInStackWithKey(noteBubbleKey);
        assertFalse(bubble.isChat());
        bubble.getIconView().callOnClick();

        // Check education is not shown
        BubbleStackView stackView = mBubbleController.getStackView();
        assertFalse(stackView.isStackEduVisible());
    }

    @Test
    public void testShowStackEdu_isConversationBubble() {
        // TODO(b/401025577): Prevent this test from raising a WTF, and remove this exemption
        mLogWtfRule.addFailureLogExemption(log-> log.getTag().equals("FloatingCoordinator"));

        // Setup
        setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, false);
        BubbleEntry bubbleEntry = createBubbleEntry();
        mBubbleController.updateBubble(bubbleEntry);
        assertTrue(mBubbleController.hasBubbles());

        // Click on bubble
        Bubble bubble = mBubbleData.getBubbleInStackWithKey(bubbleEntry.getKey());
        assertTrue(bubble.isChat());
        bubble.getIconView().callOnClick();

        // Check education is shown
        BubbleStackView stackView = mBubbleController.getStackView();
        assertTrue(stackView.isStackEduVisible());
    }

    @Test
    public void testShowStackEdu_isSeenConversationBubble() {
        // Setup
        setPrefBoolean(StackEducationView.PREF_STACK_EDUCATION, true);
        BubbleEntry bubbleEntry = createBubbleEntry();
        mBubbleController.updateBubble(bubbleEntry);
        assertTrue(mBubbleController.hasBubbles());

        // Click on bubble
        Bubble bubble = mBubbleData.getBubbleInStackWithKey(bubbleEntry.getKey());
        assertTrue(bubble.isChat());
        bubble.getIconView().callOnClick();

        // Check education is not shown
        BubbleStackView stackView = mBubbleController.getStackView();
        assertFalse(stackView.isStackEduVisible());
    }

    @Test
    public void testShowOrHideNotesBubble_addsAndExpand() {
        assertThat(mBubbleController.isStackExpanded()).isFalse();

        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);

        verify(mBubbleController).inflateAndAdd(any(Bubble.class), /* suppressFlyout= */ eq(true),
                /* showInShade= */ eq(false));
        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
                Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
        assertThat(mBubbleController.isStackExpanded()).isTrue();
    }

    @Test
    public void testShowOrHideNotesBubble_expandIfCollapsed() {
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.collapseStack();
        assertThat(mBubbleController.isStackExpanded()).isFalse();

        // Calling this while collapsed will expand the app bubble
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);

        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
                Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
        assertThat(mBubbleController.isStackExpanded()).isTrue();
        assertThat(mBubbleData.getBubbles().size()).isEqualTo(2);
    }

    @Test
    public void testShowOrHideNotesBubble_collapseIfSelected() {
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
                Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
        assertThat(mBubbleController.isStackExpanded()).isTrue();

        // Calling this while the app bubble is expanded should collapse the stack
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);

        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
                Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
        assertThat(mBubbleController.isStackExpanded()).isFalse();
        assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
        assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(mUser0);
    }

    @Test
    public void testShowOrHideNotesBubbleWithNonPrimaryUser_bubbleCollapsedWithExpectedUser() {
        UserHandle user10 = createUserHandle(/* userId = */ 10);
        String notesKey = Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), user10);
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, user10, mNotesBubbleIcon);
        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(notesKey);
        assertThat(mBubbleController.isStackExpanded()).isTrue();
        assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
        assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(user10);

        // Calling this while the app bubble is expanded should collapse the stack
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, user10, mNotesBubbleIcon);

        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(notesKey);
        assertThat(mBubbleController.isStackExpanded()).isFalse();
        assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
        assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(user10);
    }

    @Test
    public void testShowOrHideNotesBubbleOnUser10AndThenUser0_user0BubbleExpanded() {
        UserHandle user10 = createUserHandle(/* userId = */ 10);
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, user10, mNotesBubbleIcon);

        String notesBubbleUser0Key = Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(),
                mUser0);
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);

        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(notesBubbleUser0Key);
        assertThat(mBubbleController.isStackExpanded()).isTrue();
        assertThat(mBubbleData.getBubbles()).hasSize(2);
        assertThat(mBubbleData.getBubbles().get(0).getUser()).isEqualTo(mUser0);
        assertThat(mBubbleData.getBubbles().get(1).getUser()).isEqualTo(user10);
    }

    @Test
    public void testShowOrHideNotesBubble_selectIfNotSelected() {
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.expandStackAndSelectBubble(mBubbleEntry);
        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(mBubbleEntry.getKey());
        assertThat(mBubbleController.isStackExpanded()).isTrue();

        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(
                Bubble.getNoteBubbleKeyForApp(mContext.getPackageName(), mUser0));
        assertThat(mBubbleController.isStackExpanded()).isTrue();
        assertThat(mBubbleData.getBubbles().size()).isEqualTo(2);
    }

    @Test
    public void testShowOrHideNotesBubble_addsFromOverflow() {
        String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(mNotesBubbleIntent.getPackage(),
                mUser0);
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
        // Collapse the stack so we don't need to wait for the dismiss animation in the test
        mBubbleController.collapseStack();

        // Dismiss the app bubble so it's in the overflow
        mBubbleController.dismissBubble(noteBubbleKey, Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNotNull();

        // Calling this while collapsed will re-add and expand the app bubble
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(noteBubbleKey);
        assertThat(mBubbleController.isStackExpanded()).isTrue();
        assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
        assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNull();
    }

    @Test
    public void testShowOrHideNotesBubble_updateExistedBubbleInOverflow_updateIntentInBubble() {
        String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(mNotesBubbleIntent.getPackage(),
                mUser0);
        mBubbleController.showOrHideNotesBubble(mNotesBubbleIntent, mUser0, mNotesBubbleIcon);
        // Collapse the stack so we don't need to wait for the dismiss animation in the test
        mBubbleController.collapseStack();
        // Dismiss the app bubble so it's in the overflow
        mBubbleController.dismissBubble(noteBubbleKey, Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNotNull();

        // Modify the intent to include new extras.
        Intent newIntent = new Intent(mContext, BubblesTestActivity.class)
                .setPackage(mContext.getPackageName())
                .putExtra("hello", "world");

        // Calling this while collapsed will re-add and expand the app bubble
        mBubbleController.showOrHideNotesBubble(newIntent, mUser0, mNotesBubbleIcon);
        assertThat(mBubbleData.getSelectedBubble().getKey()).isEqualTo(noteBubbleKey);
        assertThat(mBubbleController.isStackExpanded()).isTrue();
        assertThat(mBubbleData.getBubbles().size()).isEqualTo(1);
        assertThat(mBubbleData.getBubbles().get(0).getIntent()
                .getStringExtra("hello")).isEqualTo("world");
        assertThat(mBubbleData.getOverflowBubbleWithKey(noteBubbleKey)).isNull();
    }

    @Test
    public void testCreateBubbleFromOngoingNotification() {
        NotificationEntry notif = new NotificationEntryBuilder()
                .setFlag(mContext, Notification.FLAG_ONGOING_EVENT, true)
                .setCanBubble(true)
                .build();

        BubbleEntry bubble = mBubblesManager.notifToBubbleEntry(notif);

        assertTrue("Ongoing Notifis should be dismissable", bubble.isDismissable());
    }


    @Test
    public void testCreateBubbleFromNoDismissNotification() {
        NotificationEntry notif = new NotificationEntryBuilder()
                .setFlag(mContext, Notification.FLAG_NO_DISMISS, true)
                .setCanBubble(true)
                .build();

        BubbleEntry bubble = mBubblesManager.notifToBubbleEntry(notif);

        assertFalse("FLAG_NO_DISMISS Notifs should be non-dismissable", bubble.isDismissable());
    }

    @DisableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void registerBubbleBarListener_barDisabled_largeScreen_shouldBeIgnored() {
        mPositioner.setIsLargeScreen(true);
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());

        assertStackMode();

        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        assertStackMode();

        assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1);
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void registerBubbleBarListener_barEnabled_smallScreen_shouldBeIgnored() {
        mPositioner.setIsLargeScreen(false);
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());

        assertStackMode();

        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        assertStackMode();

        assertThat(mBubbleController.getStackView().getBubbleCount()).isEqualTo(1);
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void registerBubbleBarListener_switchToBarAndBackToStack() {
        mPositioner.setIsLargeScreen(true);
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);
        assertTrue(mBubbleController.hasBubbles());

        assertStackMode();

        assertThat(mBubbleData.getBubbles()).hasSize(1);
        assertBubbleIsInflatedForStack(mBubbleData.getBubbles().get(0));
        assertBubbleIsInflatedForStack(mBubbleData.getOverflow());

        mBubbleController.setLauncherHasBubbleBar(true);
        assertBarMode();

        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        assertThat(mBubbleData.getBubbles()).hasSize(1);
        assertBubbleIsInflatedForBar(mBubbleData.getBubbles().get(0));
        assertBubbleIsInflatedForBar(mBubbleData.getOverflow());

        mBubbleController.unregisterBubbleStateListener();
        // Check that bubbles stay in bar mode until launcher switches back to stack
        assertBarMode();
        mBubbleController.setLauncherHasBubbleBar(false);
        assertStackMode();

        assertThat(mBubbleData.getBubbles()).hasSize(1);
        assertBubbleIsInflatedForStack(mBubbleData.getBubbles().get(0));
        assertBubbleIsInflatedForStack(mBubbleData.getOverflow());
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void registerBubbleBarListener_switchToBarWhileExpanded() {
        mPositioner.setIsLargeScreen(true);

        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);
        BubbleStackView stackView = mBubbleController.getStackView();
        spyOn(stackView);

        mBubbleData.setExpanded(true);

        assertStackMode();
        assertThat(mBubbleData.isExpanded()).isTrue();
        assertThat(stackView.isExpanded()).isTrue();

        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.setLauncherHasBubbleBar(true);
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        BubbleBarLayerView layerView = mBubbleController.getLayerView();
        spyOn(layerView);

        assertBarMode();
        assertThat(mBubbleData.isExpanded()).isTrue();
        assertThat(layerView.isExpanded()).isTrue();
    }

    @DisableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void switchBetweenBarAndStack_noBubbles_shouldBeIgnored() {
        mPositioner.setIsLargeScreen(true);
        assertFalse(mBubbleController.hasBubbles());

        assertNoBubbleContainerViews();

        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.setLauncherHasBubbleBar(true);
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        assertNoBubbleContainerViews();

        mBubbleController.unregisterBubbleStateListener();
        mBubbleController.setLauncherHasBubbleBar(false);

        assertNoBubbleContainerViews();
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void bubbleBarBubbleExpandedAndCollapsed() {
        mPositioner.setIsLargeScreen(true);
        mEntryListener.onEntryAdded(mEntry);
        mBubbleController.updateBubble(mBubbleEntry);

        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);
        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 1000);

        assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();

        mBubbleController.collapseStack();

        assertThat(mBubbleController.getLayerView().isExpanded()).isFalse();
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void dragBubbleBarBubble_selectedBubble_expandedViewCollapsesDuringDrag() {
        mPositioner.setIsLargeScreen(true);
        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        // Add 2 bubbles
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryAdded(mEntry2);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.updateBubble(mBubbleEntry2);

        // Select first bubble
        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 0);
        assertThat(mBubbleData.getSelectedBubbleKey()).isEqualTo(mBubbleEntry.getKey());
        assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();

        // Drag first bubble, bubble should collapse
        mBubbleController.startBubbleDrag(mBubbleEntry.getKey());
        assertThat(mBubbleController.getLayerView().isExpanded()).isFalse();

        // Stop dragging, first bubble should be expanded
        mBubbleController.stopBubbleDrag(BubbleBarLocation.LEFT, 0);
        assertThat(mBubbleData.getSelectedBubbleKey()).isEqualTo(mBubbleEntry.getKey());
        assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void dragBubbleBarBubble_unselectedBubble_expandedViewCollapsesDuringDrag() {
        mPositioner.setIsLargeScreen(true);
        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        // Add 2 bubbles
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryAdded(mEntry2);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.updateBubble(mBubbleEntry2);

        // Select first bubble
        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 0);
        assertThat(mBubbleData.getSelectedBubbleKey()).isEqualTo(mBubbleEntry.getKey());
        assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();

        // Drag second bubble, bubble should collapse
        mBubbleController.startBubbleDrag(mBubbleEntry2.getKey());
        assertThat(mBubbleController.getLayerView().isExpanded()).isFalse();

        // Stop dragging, first bubble should be expanded
        mBubbleController.stopBubbleDrag(BubbleBarLocation.LEFT, 0);
        assertThat(mBubbleData.getSelectedBubbleKey()).isEqualTo(mBubbleEntry.getKey());
        assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void dismissBubbleBarBubble_selected_selectsAndExpandsNext() {
        mPositioner.setIsLargeScreen(true);
        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        // Add 2 bubbles
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryAdded(mEntry2);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.updateBubble(mBubbleEntry2);

        // Select first bubble
        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 0);
        // Drag first bubble to dismiss
        mBubbleController.startBubbleDrag(mBubbleEntry.getKey());
        mBubbleController.dragBubbleToDismiss(mBubbleEntry.getKey(), System.currentTimeMillis());
        // Second bubble is selected and expanded
        assertThat(mBubbleData.getSelectedBubbleKey()).isEqualTo(mBubbleEntry2.getKey());
        assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void dismissBubbleBarBubble_unselected_selectionDoesNotChange() {
        mPositioner.setIsLargeScreen(true);
        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        // Add 2 bubbles
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryAdded(mEntry2);
        mBubbleController.updateBubble(mBubbleEntry);
        mBubbleController.updateBubble(mBubbleEntry2);

        // Select first bubble
        mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 0);
        // Drag second bubble to dismiss
        mBubbleController.startBubbleDrag(mBubbleEntry2.getKey());
        mBubbleController.dragBubbleToDismiss(mBubbleEntry2.getKey(), System.currentTimeMillis());
        // First bubble remains selected and expanded
        assertThat(mBubbleData.getSelectedBubbleKey()).isEqualTo(mBubbleEntry.getKey());
        assertThat(mBubbleController.getLayerView().isExpanded()).isTrue();
    }

    @DisableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
    @Test
    public void doesNotRegisterSensitiveStateListener() {
        verifyNoMoreInteractions(mSensitiveNotificationProtectionController);
    }

    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
    @Test
    public void registerSensitiveStateListener() {
        verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any());
    }

    @EnableFlags(FLAG_SCREENSHARE_NOTIFICATION_HIDING)
    @Test
    public void onSensitiveNotificationProtectionStateChanged() {
        verify(mSensitiveNotificationProtectionController, atLeastOnce())
                .registerSensitiveStateListener(mSensitiveStateChangedListener.capture());

        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(true);
        mSensitiveStateChangedListener.getValue().run();
        verify(mBubbleController).onSensitiveNotificationProtectionStateChanged(true);

        when(mSensitiveNotificationProtectionController.isSensitiveStateActive()).thenReturn(false);
        mSensitiveStateChangedListener.getValue().run();
        verify(mBubbleController).onSensitiveNotificationProtectionStateChanged(false);
    }

    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void setBubbleBarLocation_listenerNotified() {
        mPositioner.setIsLargeScreen(true);

        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);
        mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT,
                BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW);
        assertThat(bubbleStateListener.mLastUpdate).isNotNull();
        assertThat(bubbleStateListener.mLastUpdate.bubbleBarLocation).isEqualTo(
                BubbleBarLocation.LEFT);
    }

    @DisableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void setBubbleBarLocation_barDisabled_shouldBeIgnored() {
        mPositioner.setIsLargeScreen(true);

        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);
        mBubbleController.setBubbleBarLocation(BubbleBarLocation.LEFT,
                BubbleBarLocation.UpdateSource.DRAG_EXP_VIEW);
        assertThat(bubbleStateListener.mStateChangeCalls).isEqualTo(0);
    }

    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
    @Test
    public void showBubbleOverflow_hasOverflowContents() {
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.App);
        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();

        BubbleStackView stackView = mBubbleController.getStackView();
        spyOn(stackView);

        // Dismiss the bubble so it's in the overflow
        mBubbleController.removeBubble(mEntry.getKey(), Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();

        verify(stackView).showOverflow(eq(true));
    }

    @EnableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
    @Test
    public void showBubbleOverflow_isEmpty() {
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.App);
        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();

        BubbleStackView stackView = mBubbleController.getStackView();
        spyOn(stackView);

        // Dismiss the bubble so it's in the overflow
        mBubbleController.removeBubble(mEntry.getKey(), Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();
        verify(stackView).showOverflow(eq(true));

        // Cancel the bubble so it's removed from the overflow
        mBubbleController.removeBubble(mEntry.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();
        verify(stackView).showOverflow(eq(false));
    }

    @DisableFlags(Flags.FLAG_ENABLE_OPTIONAL_BUBBLE_OVERFLOW)
    @Test
    public void showBubbleOverflow_ignored() {
        mEntryListener.onEntryAdded(mEntry);
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.App);
        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();

        BubbleStackView stackView = mBubbleController.getStackView();
        spyOn(stackView);

        // Dismiss the bubble so it's in the overflow
        mBubbleController.removeBubble(mEntry.getKey(), Bubbles.DISMISS_USER_GESTURE);
        assertThat(mBubbleData.getOverflowBubbles()).isNotEmpty();

        // Cancel the bubble so it's removed from the overflow
        mBubbleController.removeBubble(mEntry.getKey(), Bubbles.DISMISS_NOTIF_CANCEL);
        assertThat(mBubbleData.getOverflowBubbles()).isEmpty();

        // Show overflow should never be called if the flag is off
        verify(stackView, never()).showOverflow(anyBoolean());
    }

    // TODO (b/216523800): There's a test in BubbleControllerBubbleBarTest verifying this logging,
    //  however, it doesn't do it via the notification entry listener, it calls the method within
    //  BubbleController that would eventually be called. We can remove this test from BubblesTest
    //  once we have something that validates entryListener#onEntryUpdated -> calls what's needed
    //  in BubbleController.
    @EnableFlags(FLAG_ENABLE_BUBBLE_BAR)
    @Test
    public void testEventLogging_bubbleBar_updateBubble() {
        mPositioner.setIsLargeScreen(true);
        mBubbleController.setLauncherHasBubbleBar(true);
        FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener();
        mBubbleController.registerBubbleStateListener(bubbleStateListener);

        mEntryListener.onEntryAdded(mEntry);
        // Mark the notification as updated
        NotificationEntryHelper.modifyRanking(mEntry).setTextChanged(true).build();
        mEntryListener.onEntryUpdated(mEntry, /* source= */ UpdateSource.App);

        verify(mBubbleLogger).log(eqBubbleWithKey(mEntry.getKey()),
                eq(BubbleLogger.Event.BUBBLE_BAR_BUBBLE_UPDATED));
    }

    /** Creates a bubble using the userId and package. */
    private Bubble createBubble(int userId, String pkg) {
        final UserHandle userHandle = new UserHandle(userId);
        NotificationEntry workEntry = new NotificationEntryBuilder()
                .setPkg(pkg)
                .setUser(userHandle)
                .build();
        workEntry.setBubbleMetadata(getMetadata());
        workEntry.setFlagBubble(true);

        SyncExecutor executor = new SyncExecutor();
        return new Bubble(mBubblesManager.notifToBubbleEntry(workEntry),
                null,
                mock(Bubbles.PendingIntentCanceledListener.class), executor, executor);
    }

    /**
     * Creates a BubbleEntry, which will represent a chat bubble.
     * All bubble entries are notification based & therefore are chat bubbles.
     */
    private BubbleEntry createBubbleEntry() {
        NotificationEntry entry = mKosmos.createBubbledEntry(builder -> {
            return builder.done();
        });
        return mBubblesManager.notifToBubbleEntry(entry);
    }

    /** Creates a context that will return a PackageManager with specific AppInfo. */
    private Context setUpContextWithPackageManager(String pkg, ApplicationInfo info)
            throws Exception {
        final PackageManager pm = mock(PackageManager.class);
        when(pm.getApplicationInfo(eq(pkg), anyInt())).thenReturn(info);

        if (info != null) {
            Drawable d = mock(Drawable.class);
            when(d.getBounds()).thenReturn(new Rect());
            when(pm.getApplicationIcon(anyString())).thenReturn(d);
            when(pm.getUserBadgedIcon(any(), any())).thenReturn(d);
        }

        final Context context = mock(Context.class);
        when(context.getPackageName()).thenReturn(pkg);
        when(context.getPackageManager()).thenReturn(pm);
        return context;
    }

    /**
     * Sets the bubble metadata flags for this entry. These flags are normally set by
     * NotificationManagerService when the notification is sent, however, these tests do not
     * go through that path so we set them explicitly when testing.
     */
    private void setMetadataFlags(NotificationEntry entry, int flag, boolean enableFlag) {
        Notification.BubbleMetadata bubbleMetadata =
                entry.getSbn().getNotification().getBubbleMetadata();
        int flags = bubbleMetadata.getFlags();
        if (enableFlag) {
            flags |= flag;
        } else {
            flags &= ~flag;
        }
        bubbleMetadata.setFlags(flags);
    }

    /**
     * Set preferences boolean value for key
     * Used to setup global state for stack view education tests
     */
    private void setPrefBoolean(String key, boolean enabled) {
        mContext.getSharedPreferences(mContext.getPackageName(), Context.MODE_PRIVATE)
                .edit().putBoolean(key, enabled).apply();
    }

    private Notification.BubbleMetadata getMetadata() {
        Intent target = new Intent(mContext, BubblesTestActivity.class);
        PendingIntent bubbleIntent = PendingIntent.getActivity(mContext, 0, target, FLAG_MUTABLE);
        return new Notification.BubbleMetadata.Builder(
                bubbleIntent,
                Icon.createWithResource(
                        mContext,
                        com.android.wm.shell.R.drawable.bubble_ic_create_bubble))
                .build();
    }

    private void switchUser(int userId) {
        when(mLockscreenUserManager.isCurrentProfile(anyInt())).thenAnswer(
                (Answer<Boolean>) invocation -> invocation.<Integer>getArgument(0) == userId);
        SparseArray<UserInfo> userInfos = new SparseArray<>(1);
        userInfos.put(userId, mock(UserInfo.class));
        mBubbleController.onCurrentProfilesChanged(userInfos);
        mBubbleController.onUserChanged(userId);
    }

    private UserHandle createUserHandle(int userId) {
        UserHandle user = mock(UserHandle.class);
        when(user.getIdentifier()).thenReturn(userId);
        return user;
    }

    /**
     * Asserts that the bubble stack is expanded and also validates the cached state is updated.
     */
    private void assertStackExpanded() {
        assertTrue(mBubbleController.isStackExpanded());
        assertTrue(mBubbleController.getImplCachedState().isStackExpanded());
    }

    /**
     * Asserts that the bubble stack is collapsed and also validates the cached state is updated.
     */
    private void assertStackCollapsed() {
        assertFalse(mBubbleController.isStackExpanded());
        assertFalse(mBubbleController.getImplCachedState().isStackExpanded());
    }

    /** Asserts that both the bubble stack and bar views don't exist. */
    private void assertNoBubbleContainerViews() {
        assertThat(mBubbleController.getStackView()).isNull();
        assertThat(mBubbleController.getLayerView()).isNull();
    }

    /** Asserts that the stack is created and the bar is null. */
    private void assertStackMode() {
        assertThat(mBubbleController.getStackView()).isNotNull();
        assertThat(mBubbleController.getLayerView()).isNull();
    }

    /** Asserts that the given bubble has the stack expanded view inflated. */
    private void assertBubbleIsInflatedForStack(BubbleViewProvider b) {
        assertThat(b.getIconView()).isNotNull();
        assertThat(b.getExpandedView()).isNotNull();
        assertThat(b.getBubbleBarExpandedView()).isNull();
    }

    /** Asserts that the bar is created and the stack is null. */
    private void assertBarMode() {
        assertThat(mBubbleController.getStackView()).isNull();
        assertThat(mBubbleController.getLayerView()).isNotNull();
    }

    /** Asserts that the given bubble has the bar expanded view inflated. */
    private void assertBubbleIsInflatedForBar(BubbleViewProvider b) {
        // the icon view should be inflated for the overflow but not for other bubbles when showing
        // in the bar
        if (b instanceof Bubble) {
            assertThat(b.getIconView()).isNull();
        } else if (b instanceof BubbleOverflow) {
            assertThat(b.getIconView()).isNotNull();
        }
        assertThat(b.getExpandedView()).isNull();
        assertThat(b.getBubbleBarExpandedView()).isNotNull();
    }

    /**
     * Asserts that a bubble notification is suppressed from the shade and also validates the cached
     * state is updated.
     */
    private void assertBubbleNotificationSuppressedFromShade(BubbleEntry entry) {
        assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
                entry.getKey(), entry.getGroupKey()));
        assertTrue(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                entry.getKey(), entry.getGroupKey()));
    }

    /**
     * Asserts that a bubble notification is not suppressed from the shade and also validates the
     * cached state is updated.
     */
    private void assertBubbleNotificationNotSuppressedFromShade(BubbleEntry entry) {
        assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
                entry.getKey(), entry.getGroupKey()));
        assertFalse(mBubbleController.getImplCachedState().isBubbleNotificationSuppressedFromShade(
                entry.getKey(), entry.getGroupKey()));
    }

    /**
     * Asserts that the system ui states associated to bubbles are in the correct state.
     */
    private void assertSysuiStates(boolean stackExpanded, boolean manageMenuExpanded) {
        assertThat(mSysUiStateBubblesExpanded).isEqualTo(stackExpanded);
        assertThat(mSysUiStateBubblesManageMenuExpanded).isEqualTo(manageMenuExpanded);
    }

    private Bubble eqBubbleWithKey(String key) {
        return argThat(b -> b.getKey().equals(key));
    }

    private static class FakeBubbleStateListener implements Bubbles.BubbleStateListener {

        int mStateChangeCalls = 0;
        @Nullable
        BubbleBarUpdate mLastUpdate;

        @Override
        public void onBubbleStateChange(BubbleBarUpdate update) {
            mStateChangeCalls++;
            mLastUpdate = update;
        }

        @Override
        public void animateBubbleBarLocation(BubbleBarLocation location) {
        }

        @Override
        public void showBubbleBarDropTargetAt(@Nullable BubbleBarLocation location) {

        }
    }
}
