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); + } + +}