From 225b712359a13d4dfda1bb0c2a13a87ec3c824a7 Mon Sep 17 00:00:00 2001 From: Kevin Han Date: Fri, 31 Jan 2020 15:42:42 -0800 Subject: [PATCH] Move inflation-related NEM tests to their own file In a gambit to improve organization and make other improvements to NotificationEntryManagerTest, this CL moves the inflation-related tests to a separate file, leaving more focused unit tests in NotificationEntryManagerTest. In the process of doing this, I've also tried to make net positive changes to how the inflation test works. The summary of changes below. The Good * We no longer modify NotificationContentInflater behavior for tests by telling it to enter "synchronous" mode which probably means we no longer have to maintain a test-specific path of logic in that class * We no longer depend on a countdown latch attached as a listener to know when inflation is done, so listeners breaking shouldn't break all the inflation-related tests. * We no longer have the possibility of the test blocking forever from a TestableLooper#processMessages waiting for a message forever (we just throw a runtime exception now if it times out) * Hopefully less flaky since we have tighter control/knowledge on what messages we produce/wait on. The Bad * We now have to use the main thread looper. This is because using NotificationContentInflater "for real" uses a lot of AsyncTasks which HAVE to post back to main thread. Since we split the tests though, this is only needed for inflation-related tests which arguably should be on main thread. The Ugly * The test is more fragile for async/main-thread-related changes as we depend on knowing the exact number of main thread callbacks to know when inflation is done. I've tried to mitigate this as much as possible by having very explicit documentation, but this is clearly less than ideal. Test: atest SystemUITests Change-Id: Iff5413cc48167e182f9fdfa0606790c0d4071154 --- .../row/ExpandableNotificationRow.java | 1 + .../NotificationEntryManagerTest.java | 198 +-------- ...NotificationEntryManagerInflationTest.java | 385 ++++++++++++++++++ 3 files changed, 403 insertions(+), 181 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index 234ab936345b8..500813318484c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -482,6 +482,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView * once per notification as the packageInfo can't technically change for a notification row. */ private void cacheIsSystemNotification() { + //TODO: This probably shouldn't be in ExpandableNotificationRow if (mEntry != null && mEntry.mIsSystemNotification == null) { if (mSystemNotificationAsyncTask.getStatus() == AsyncTask.Status.PENDING) { // Run async task once, only if it hasn't already been executed. Note this is diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java index 6a6e5c8973188..98f12ce06e493 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java @@ -52,73 +52,55 @@ import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.ArraySet; -import android.widget.FrameLayout; import androidx.annotation.NonNull; import androidx.test.filters.SmallTest; import com.android.internal.statusbar.NotificationVisibility; -import com.android.internal.util.NotificationMessagingUtil; import com.android.systemui.Dependency; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; -import com.android.systemui.plugins.FalsingManager; -import com.android.systemui.plugins.statusbar.StatusBarStateController; -import com.android.systemui.shared.plugins.PluginManager; import com.android.systemui.statusbar.FeatureFlags; import com.android.systemui.statusbar.NotificationLifetimeExtender; -import com.android.systemui.statusbar.NotificationLockscreenUserManager; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.NotificationRemoveInterceptor; import com.android.systemui.statusbar.RankingBuilder; -import com.android.systemui.statusbar.RemoteInputController; import com.android.systemui.statusbar.SmartReplyController; -import com.android.systemui.statusbar.StatusBarIconView; import com.android.systemui.statusbar.notification.NotificationEntryManager.KeyguardEnvironment; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.NotificationRankingManager; -import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; +import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinder; import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; -import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; -import com.android.systemui.statusbar.notification.row.ActivatableNotificationViewController; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; -import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController; -import com.android.systemui.statusbar.notification.row.NotifBindPipeline; -import com.android.systemui.statusbar.notification.row.NotificationGutsManager; -import com.android.systemui.statusbar.notification.row.RowContentBindParams; -import com.android.systemui.statusbar.notification.row.RowContentBindStage; +import com.android.systemui.statusbar.notification.row.NotificationEntryManagerInflationTest; import com.android.systemui.statusbar.notification.row.RowInflaterTask; -import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; -import com.android.systemui.statusbar.notification.stack.NotificationListContainer; -import com.android.systemui.statusbar.phone.KeyguardBypassController; import com.android.systemui.statusbar.phone.NotificationGroupManager; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.util.leak.LeakDetector; -import com.android.systemui.util.time.FakeSystemClock; -import org.junit.After; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Set; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; +/** + * Unit tests for {@link NotificationEntryManager}. This test will not test any interactions with + * inflation. Instead, for functional inflation tests, see + * {@link NotificationEntryManagerInflationTest}. + */ @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper() @@ -129,17 +111,11 @@ public class NotificationEntryManagerTest extends SysuiTestCase { @Mock private NotificationPresenter mPresenter; @Mock private KeyguardEnvironment mEnvironment; @Mock private ExpandableNotificationRow mRow; - @Mock private NotificationListContainer mListContainer; @Mock private NotificationEntryListener mEntryListener; @Mock private NotificationRemoveInterceptor mRemoveInterceptor; - @Mock private NotificationRowBinderImpl.BindRowCallback mBindCallback; @Mock private HeadsUpManager mHeadsUpManager; @Mock private RankingMap mRankingMap; - @Mock private RemoteInputController mRemoteInputController; - @Mock private NotificationInterruptionStateProvider mNotificationInterruptionStateProvider; - @Mock private NotificationLockscreenUserManager mLockscreenUserManager; @Mock private NotificationGroupManager mGroupManager; - @Mock private NotificationGutsManager mGutsManager; @Mock private NotificationRemoteInputManager mRemoteInputManager; @Mock private DeviceProvisionedController mDeviceProvisionedController; @Mock private RowInflaterTask mAsyncInflationTask; @@ -147,18 +123,12 @@ public class NotificationEntryManagerTest extends SysuiTestCase { @Mock private FeatureFlags mFeatureFlags; @Mock private LeakDetector mLeakDetector; @Mock private NotificationMediaManager mNotificationMediaManager; - @Mock private ExpandableNotificationRowComponent.Builder - mExpandableNotificationRowComponentBuilder; - @Mock private ExpandableNotificationRowComponent mExpandableNotificationRowComponent; - @Mock private FalsingManager mFalsingManager; - @Mock private KeyguardBypassController mKeyguardBypassController; - @Mock private StatusBarStateController mStatusBarStateController; + @Mock private NotificationRowBinder mNotificationRowBinder; private int mId; private NotificationEntry mEntry; private StatusBarNotification mSbn; - private TestableNotificationEntryManager mEntryManager; - private CountDownLatch mCountDownLatch; + private NotificationEntryManager mEntryManager; private void setUserSentiment(String key, int sentiment) { doAnswer(invocationOnMock -> { @@ -202,41 +172,16 @@ public class NotificationEntryManagerTest extends SysuiTestCase { MockitoAnnotations.initMocks(this); mDependency.injectMockDependency(SmartReplyController.class); - mCountDownLatch = new CountDownLatch(1); - allowTestableLooperAsMainThread(); mDependency.injectTestDependency(Dependency.MAIN_HANDLER, Handler.createAsync(TestableLooper.get(this).getLooper())); - when(mRemoteInputManager.getController()).thenReturn(mRemoteInputController); - when(mListContainer.getViewParentForNotification(any())).thenReturn( - new FrameLayout(mContext)); mEntry = createNotification(); mSbn = mEntry.getSbn(); - mEntry.expandedIcon = mock(StatusBarIconView.class); - - RowContentBindStage bindStage = mock(RowContentBindStage.class); - when(bindStage.getStageParams(any())).thenReturn(new RowContentBindParams()); - NotificationRowBinderImpl notificationRowBinder = - new NotificationRowBinderImpl(mContext, - new NotificationMessagingUtil(mContext), - mRemoteInputManager, - mLockscreenUserManager, - mock(NotifBindPipeline.class), - bindStage, - true, /* allowLongPress */ - mKeyguardBypassController, - mStatusBarStateController, - mGroupManager, - mGutsManager, - mNotificationInterruptionStateProvider, - RowInflaterTask::new, - mExpandableNotificationRowComponentBuilder); - when(mFeatureFlags.isNewNotifPipelineEnabled()).thenReturn(false); when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(false); - mEntryManager = new TestableNotificationEntryManager( + mEntryManager = new NotificationEntryManager( mLogger, mGroupManager, new NotificationRankingManager( @@ -250,7 +195,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase { mock(HighPriorityProvider.class)), mEnvironment, mFeatureFlags, - () -> notificationRowBinder, + () -> mNotificationRowBinder, () -> mRemoteInputManager, mLeakDetector, mock(ForegroundServiceDismissalFeatureController.class) @@ -259,152 +204,45 @@ public class NotificationEntryManagerTest extends SysuiTestCase { mEntryManager.addNotificationEntryListener(mEntryListener); mEntryManager.addNotificationRemoveInterceptor(mRemoveInterceptor); - notificationRowBinder.setUpWithPresenter(mPresenter, mListContainer, mBindCallback); - notificationRowBinder.setInflationCallback(mEntryManager); - notificationRowBinder.setNotificationClicker(mock(NotificationClicker.class)); - - setUserSentiment( - mEntry.getKey(), Ranking.USER_SENTIMENT_NEUTRAL); - - ArgumentCaptor viewCaptor = - ArgumentCaptor.forClass(ExpandableNotificationRow.class); - when(mExpandableNotificationRowComponentBuilder - .expandableNotificationRow(viewCaptor.capture())) - .thenReturn(mExpandableNotificationRowComponentBuilder); - when(mExpandableNotificationRowComponentBuilder - .notificationEntry(any())) - .thenReturn(mExpandableNotificationRowComponentBuilder); - when(mExpandableNotificationRowComponentBuilder - .onDismissRunnable(any())) - .thenReturn(mExpandableNotificationRowComponentBuilder); - when(mExpandableNotificationRowComponentBuilder - .inflationCallback(any())) - .thenReturn(mExpandableNotificationRowComponentBuilder); - when(mExpandableNotificationRowComponentBuilder - .rowContentBindStage(any())) - .thenReturn(mExpandableNotificationRowComponentBuilder); - when(mExpandableNotificationRowComponentBuilder - .onExpandClickListener(any())) - .thenReturn(mExpandableNotificationRowComponentBuilder); - - when(mExpandableNotificationRowComponentBuilder.build()) - .thenReturn(mExpandableNotificationRowComponent); - when(mExpandableNotificationRowComponent.getExpandableNotificationRowController()) - .thenAnswer((Answer) invocation -> - new ExpandableNotificationRowController( - viewCaptor.getValue(), - mock(ActivatableNotificationViewController.class), - mNotificationMediaManager, - mock(PluginManager.class), - new FakeSystemClock(), - "FOOBAR", "FOOBAR", - mKeyguardBypassController, - mGroupManager, - bindStage, - mock(NotificationLogger.class), - mHeadsUpManager, - mPresenter, - mStatusBarStateController, - mEntryManager, - mGutsManager, - true, - null, - mFalsingManager - )); + setUserSentiment(mSbn.getKey(), Ranking.USER_SENTIMENT_NEUTRAL); } - @After - public void tearDown() { - // CLEAN UP inflation tasks so they don't callback in a future test - mEntry.abortTask(); - } - - // TODO: These tests are closer to functional tests and we should move them to their own file. - // and also strip some of the verifies that make the test too complex @Test - @Ignore - public void testAddNotification() throws Exception { - TestableLooper.get(this).processAllMessages(); - - doAnswer(invocation -> { - mCountDownLatch.countDown(); - return null; - }).when(mBindCallback).onBindRow(any(), any(), any(), any()); - - // Post on main thread, otherwise we will be stuck waiting here for the inflation finished - // callback forever, since it won't execute until the tests ends. + public void testAddNotification_setsUserSentiment() { mEntryManager.addNotification(mSbn, mRankingMap); - TestableLooper.get(this).processMessages(1); - assertTrue(mCountDownLatch.await(10, TimeUnit.SECONDS)); - assertTrue(mEntryManager.getCountDownLatch().await(10, TimeUnit.SECONDS)); - // Check that no inflation error occurred. - verify(mEntryListener, never()).onInflationError(any(), any()); - - // Row inflation: ArgumentCaptor entryCaptor = ArgumentCaptor.forClass( NotificationEntry.class); - verify(mBindCallback).onBindRow(entryCaptor.capture(), any(), eq(mSbn), any()); + verify(mEntryListener).onPendingEntryAdded(entryCaptor.capture()); NotificationEntry entry = entryCaptor.getValue(); - verify(mRemoteInputManager).bindRow(entry.getRow()); - // Row content inflation: - verify(mEntryListener).onNotificationAdded(entry); - verify(mPresenter).updateNotificationViews(); - - assertEquals(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey()), entry); - assertNotNull(entry.getRow()); - assertEquals(mEntry.getUserSentiment(), - Ranking.USER_SENTIMENT_NEUTRAL); + assertEquals(entry.getUserSentiment(), Ranking.USER_SENTIMENT_NEUTRAL); } @Test - @Ignore - public void testUpdateNotification() throws Exception { - TestableLooper.get(this).processAllMessages(); - + public void testUpdateNotification_updatesUserSentiment() { mEntryManager.addActiveNotificationForTest(mEntry); - setUserSentiment( mEntry.getKey(), Ranking.USER_SENTIMENT_NEGATIVE); mEntryManager.updateNotification(mSbn, mRankingMap); - TestableLooper.get(this).processMessages(1); - // Wait for content update. - assertTrue(mEntryManager.getCountDownLatch().await(10, TimeUnit.SECONDS)); - verify(mEntryListener, never()).onInflationError(any(), any()); - - verify(mEntryListener).onPreEntryUpdated(mEntry); - verify(mPresenter).updateNotificationViews(); - verify(mEntryListener).onPostEntryUpdated(mEntry); - - assertNotNull(mEntry.getRow()); - assertEquals(Ranking.USER_SENTIMENT_NEGATIVE, - mEntry.getUserSentiment()); + assertEquals(Ranking.USER_SENTIMENT_NEGATIVE, mEntry.getUserSentiment()); } @Test - @Ignore public void testUpdateNotification_prePostEntryOrder() throws Exception { TestableLooper.get(this).processAllMessages(); mEntryManager.addActiveNotificationForTest(mEntry); mEntryManager.updateNotification(mSbn, mRankingMap); - TestableLooper.get(this).processMessages(1); - // Wait for content update. - assertTrue(mEntryManager.getCountDownLatch().await(10, TimeUnit.SECONDS)); - - verify(mEntryListener, never()).onInflationError(any(), any()); // Ensure that update callbacks happen in correct order InOrder order = inOrder(mEntryListener, mPresenter, mEntryListener); order.verify(mEntryListener).onPreEntryUpdated(mEntry); order.verify(mPresenter).updateNotificationViews(); order.verify(mEntryListener).onPostEntryUpdated(mEntry); - - assertNotNull(mEntry.getRow()); } @Test @@ -414,8 +252,6 @@ public class NotificationEntryManagerTest extends SysuiTestCase { mEntryManager.removeNotification(mSbn.getKey(), mRankingMap, UNDEFINED_DISMISS_REASON); - verify(mEntryListener, never()).onInflationError(any(), any()); - verify(mPresenter).updateNotificationViews(); verify(mEntryListener).onEntryRemoved( eq(mEntry), any(), eq(false) /* removedByUser */); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java new file mode 100644 index 0000000000000..ac40808686534 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationEntryManagerInflationTest.java @@ -0,0 +1,385 @@ +/* + * 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.statusbar.notification.row; + +import static android.app.NotificationManager.IMPORTANCE_DEFAULT; + +import static junit.framework.Assert.assertNotNull; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.Notification; +import android.os.Handler; +import android.service.notification.NotificationListenerService; +import android.service.notification.NotificationListenerService.Ranking; +import android.service.notification.StatusBarNotification; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; + +import androidx.test.filters.SmallTest; + +import com.android.internal.util.NotificationMessagingUtil; +import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.plugins.FalsingManager; +import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.shared.plugins.PluginManager; +import com.android.systemui.statusbar.FeatureFlags; +import com.android.systemui.statusbar.NotificationLockscreenUserManager; +import com.android.systemui.statusbar.NotificationMediaManager; +import com.android.systemui.statusbar.NotificationPresenter; +import com.android.systemui.statusbar.NotificationRemoteInputManager; +import com.android.systemui.statusbar.SbnBuilder; +import com.android.systemui.statusbar.SmartReplyController; +import com.android.systemui.statusbar.notification.ForegroundServiceDismissalFeatureController; +import com.android.systemui.statusbar.notification.NotificationClicker; +import com.android.systemui.statusbar.notification.NotificationEntryListener; +import com.android.systemui.statusbar.notification.NotificationEntryManager; +import com.android.systemui.statusbar.notification.NotificationEntryManagerLogger; +import com.android.systemui.statusbar.notification.NotificationFilter; +import com.android.systemui.statusbar.notification.NotificationInterruptionStateProvider; +import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager; +import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.collection.NotificationRankingManager; +import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl; +import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider; +import com.android.systemui.statusbar.notification.logging.NotificationLogger; +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag; +import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; +import com.android.systemui.statusbar.notification.row.dagger.NotificationRowComponent; +import com.android.systemui.statusbar.notification.stack.NotificationListContainer; +import com.android.systemui.statusbar.phone.KeyguardBypassController; +import com.android.systemui.statusbar.phone.NotificationGroupManager; +import com.android.systemui.statusbar.policy.HeadsUpManager; +import com.android.systemui.statusbar.policy.SmartReplyConstants; +import com.android.systemui.util.leak.LeakDetector; +import com.android.systemui.util.time.FakeSystemClock; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; +import org.mockito.stubbing.Answer; + +/** + * Functional tests for notification inflation from {@link NotificationEntryManager}. + */ +@SmallTest +@RunWith(AndroidTestingRunner.class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +public class NotificationEntryManagerInflationTest extends SysuiTestCase { + + private static final String TEST_TITLE = "Title"; + private static final String TEST_TEXT = "Text"; + private static final long TIMEOUT_TIME = 10000; + private static final Runnable TIMEOUT_RUNNABLE = () -> { + throw new RuntimeException("Timed out waiting to inflate"); + }; + + @Mock private NotificationPresenter mPresenter; + @Mock private NotificationEntryManager.KeyguardEnvironment mEnvironment; + @Mock private NotificationListContainer mListContainer; + @Mock private NotificationEntryListener mEntryListener; + @Mock private NotificationRowBinderImpl.BindRowCallback mBindCallback; + @Mock private HeadsUpManager mHeadsUpManager; + @Mock private NotificationInterruptionStateProvider mNotificationInterruptionStateProvider; + @Mock private NotificationLockscreenUserManager mLockscreenUserManager; + @Mock private NotificationGutsManager mGutsManager; + @Mock private NotificationRemoteInputManager mRemoteInputManager; + @Mock private NotificationMediaManager mNotificationMediaManager; + @Mock private ExpandableNotificationRowComponent.Builder + mExpandableNotificationRowComponentBuilder; + @Mock private ExpandableNotificationRowComponent mExpandableNotificationRowComponent; + @Mock private FalsingManager mFalsingManager; + @Mock private KeyguardBypassController mKeyguardBypassController; + @Mock private StatusBarStateController mStatusBarStateController; + + @Mock private NotificationGroupManager mGroupManager; + @Mock private FeatureFlags mFeatureFlags; + @Mock private LeakDetector mLeakDetector; + + @Mock private ActivatableNotificationViewController mActivatableNotificationViewController; + @Mock private NotificationRowComponent.Builder mNotificationRowComponentBuilder; + + private StatusBarNotification mSbn; + private NotificationListenerService.RankingMap mRankingMap; + private NotificationEntryManager mEntryManager; + private NotificationRowBinderImpl mRowBinder; + private Handler mHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + mDependency.injectMockDependency(SmartReplyController.class); + + mHandler = Handler.createAsync(TestableLooper.get(this).getLooper()); + + Notification notification = new Notification.Builder(mContext) + .setSmallIcon(R.drawable.ic_person) + .setContentTitle(TEST_TITLE) + .setContentText(TEST_TEXT) + .build(); + mSbn = new SbnBuilder() + .setNotification(notification) + .build(); + + when(mFeatureFlags.isNewNotifPipelineEnabled()).thenReturn(false); + when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(false); + + mEntryManager = new NotificationEntryManager( + mock(NotificationEntryManagerLogger.class), + mGroupManager, + new NotificationRankingManager( + () -> mock(NotificationMediaManager.class), + mGroupManager, + mHeadsUpManager, + mock(NotificationFilter.class), + mock(NotificationEntryManagerLogger.class), + mock(NotificationSectionsFeatureManager.class), + mock(PeopleNotificationIdentifier.class), + mock(HighPriorityProvider.class)), + mEnvironment, + mFeatureFlags, + () -> mRowBinder, + () -> mRemoteInputManager, + mLeakDetector, + mock(ForegroundServiceDismissalFeatureController.class) + ); + + NotifRemoteViewCache cache = new NotifRemoteViewCacheImpl(mEntryManager); + NotifBindPipeline pipeline = new NotifBindPipeline( + mEntryManager, + mock(NotifBindPipelineLogger.class)); + NotificationContentInflater binder = new NotificationContentInflater( + cache, + mRemoteInputManager, + () -> mock(SmartReplyConstants.class), + () -> mock(SmartReplyController.class)); + RowContentBindStage stage = new RowContentBindStage( + binder, + mock(NotifInflationErrorManager.class), + mock(RowContentBindStageLogger.class)); + pipeline.setStage(stage); + + ArgumentCaptor viewCaptor = + ArgumentCaptor.forClass(ExpandableNotificationRow.class); + when(mExpandableNotificationRowComponentBuilder + .expandableNotificationRow(viewCaptor.capture())) + .thenReturn(mExpandableNotificationRowComponentBuilder); + when(mExpandableNotificationRowComponentBuilder + .notificationEntry(any())) + .thenReturn(mExpandableNotificationRowComponentBuilder); + when(mExpandableNotificationRowComponentBuilder + .onDismissRunnable(any())) + .thenReturn(mExpandableNotificationRowComponentBuilder); + when(mExpandableNotificationRowComponentBuilder + .inflationCallback(any())) + .thenReturn(mExpandableNotificationRowComponentBuilder); + when(mExpandableNotificationRowComponentBuilder + .rowContentBindStage(any())) + .thenReturn(mExpandableNotificationRowComponentBuilder); + when(mExpandableNotificationRowComponentBuilder + .onExpandClickListener(any())) + .thenReturn(mExpandableNotificationRowComponentBuilder); + + when(mExpandableNotificationRowComponentBuilder.build()) + .thenReturn(mExpandableNotificationRowComponent); + when(mExpandableNotificationRowComponent.getExpandableNotificationRowController()) + .thenAnswer((Answer) invocation -> + new ExpandableNotificationRowController( + viewCaptor.getValue(), + mock(ActivatableNotificationViewController.class), + mNotificationMediaManager, + mock(PluginManager.class), + new FakeSystemClock(), + "FOOBAR", "FOOBAR", + mKeyguardBypassController, + mGroupManager, + stage, + mock(NotificationLogger.class), + mHeadsUpManager, + mPresenter, + mStatusBarStateController, + mEntryManager, + mGutsManager, + true, + null, + mFalsingManager + )); + + when(mNotificationRowComponentBuilder.activatableNotificationView(any())) + .thenReturn(mNotificationRowComponentBuilder); + when(mNotificationRowComponentBuilder.build()).thenReturn( + () -> mActivatableNotificationViewController); + + mRowBinder = new NotificationRowBinderImpl( + mContext, + new NotificationMessagingUtil(mContext), + mRemoteInputManager, + mLockscreenUserManager, + pipeline, + stage, + true, /* allowLongPress */ + mock(KeyguardBypassController.class), + mock(StatusBarStateController.class), + mGroupManager, + mGutsManager, + mNotificationInterruptionStateProvider, + RowInflaterTask::new, + mExpandableNotificationRowComponentBuilder); + + mEntryManager.setUpWithPresenter(mPresenter); + mEntryManager.addNotificationEntryListener(mEntryListener); + + mRowBinder.setUpWithPresenter(mPresenter, mListContainer, mBindCallback); + mRowBinder.setInflationCallback(mEntryManager); + mRowBinder.setNotificationClicker(mock(NotificationClicker.class)); + + Ranking ranking = new Ranking(); + ranking.populate( + mSbn.getKey(), + 0, + false, + 0, + 0, + IMPORTANCE_DEFAULT, + null, + null, + null, + null, + null, + true, + Ranking.USER_SENTIMENT_NEUTRAL, + false, + -1, + false, + null, + null, + false, + false, + false); + mRankingMap = new NotificationListenerService.RankingMap(new Ranking[] {ranking}); + + TestableLooper.get(this).processAllMessages(); + } + + @After + public void cleanUp() { + // Don't leave anything on main thread + TestableLooper.get(this).processAllMessages(); + } + + @Test + public void testAddNotification() { + // WHEN a notification is added + mEntryManager.addNotification(mSbn, mRankingMap); + ArgumentCaptor entryCaptor = ArgumentCaptor.forClass( + NotificationEntry.class); + verify(mEntryListener).onPendingEntryAdded(entryCaptor.capture()); + NotificationEntry entry = entryCaptor.getValue(); + + // Wait for inflation + // row inflation, system notification, remote views, contracted view + waitForMessages(4); + + // THEN the notification has its row inflated + assertNotNull(entry.getRow()); + assertNotNull(entry.getRow().getPrivateLayout().getContractedChild()); + + // THEN inflation callbacks are called + verify(mBindCallback).onBindRow(eq(entry), any(), eq(mSbn), any()); + verify(mEntryListener, never()).onInflationError(any(), any()); + verify(mEntryListener).onEntryInflated(entry); + verify(mEntryListener).onNotificationAdded(entry); + + // THEN the notification is active + assertNotNull(mEntryManager.getActiveNotificationUnfiltered(mSbn.getKey())); + + // THEN we update the presenter + verify(mPresenter).updateNotificationViews(); + } + + @Test + public void testUpdateNotification() { + // GIVEN a notification already added + mEntryManager.addNotification(mSbn, mRankingMap); + ArgumentCaptor entryCaptor = ArgumentCaptor.forClass( + NotificationEntry.class); + verify(mEntryListener).onPendingEntryAdded(entryCaptor.capture()); + NotificationEntry entry = entryCaptor.getValue(); + waitForMessages(4); + + Mockito.reset(mEntryListener); + Mockito.reset(mPresenter); + + // WHEN the notification is updated + mEntryManager.updateNotification(mSbn, mRankingMap); + + // Wait for inflation + // remote views, contracted view + waitForMessages(2); + + // THEN the notification has its row and inflated + assertNotNull(entry.getRow()); + + // THEN inflation callbacks are called + verify(mEntryListener, never()).onInflationError(any(), any()); + verify(mEntryListener).onEntryReinflated(entry); + + // THEN we update the presenter + verify(mPresenter).updateNotificationViews(); + } + + /** + * Wait for a certain number of messages to finish before continuing, timing out if they never + * occur. + * + * As part of the inflation pipeline, the main thread is forced to deal with several callbacks + * due to the nature of the API used (generally because they're {@link android.os.AsyncTask} + * callbacks). In order, these are + * + * 1) Callback after row inflation. See {@link RowInflaterTask}. + * 2) Callback checking if row is system notification. See + * {@link ExpandableNotificationRow#setEntry} + * 3) Callback after remote views are created. See + * {@link NotificationContentInflater.AsyncInflationTask}. + * 4-6) Callback after each content view is inflated/rebound from remote view. See + * {@link NotificationContentInflater#applyRemoteView} and {@link InflationFlag}. + * + * Depending on the test, only some of these will be necessary. For example, generally, not + * every content view is inflated or the row may not be inflated if one already exists. + * + * Currently, the burden is on the developer to figure these out until we have a much more + * test-friendly way of executing inflation logic (i.e. pass in an executor). + */ + private void waitForMessages(int numMessages) { + mHandler.postDelayed(TIMEOUT_RUNNABLE, TIMEOUT_TIME); + TestableLooper.get(this).processMessages(numMessages); + mHandler.removeCallbacks(TIMEOUT_RUNNABLE); + } + +}