New Pipeline: Update NotificationLogger to use NotifPipeline

Fixes: 204764064
Test: atest NotifPipelineTest NotificationLoggerTest NotificationLoggerLegacyTest
Change-Id: I4a4422a898c908439ba914703561e89b3ad81b21
This commit is contained in:
Jeff DeCew
2021-11-02 21:34:31 -04:00
parent 1c8ed43c5d
commit 5385b5c789
12 changed files with 564 additions and 172 deletions

View File

@@ -13,32 +13,25 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.statusbar.notification.collection
package com.android.systemui.statusbar.notification.collection;
import androidx.annotation.Nullable;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
import java.util.Collection;
import java.util.List;
import javax.inject.Inject;
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeFinalizeFilterListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeSortListener
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeTransformGroupsListener
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Invalidator
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifComparator
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection
import com.android.systemui.statusbar.notification.collection.notifcollection.InternalNotifUpdater
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import javax.inject.Inject
/**
* The system that constructs the "shade list", the filtered, grouped, and sorted list of
@@ -50,42 +43,33 @@ import javax.inject.Inject;
* This list differs from the canonical one we receive from system server in a few ways:
* - Filtered: Some notifications are filtered out. For example, we filter out notifications whose
* views haven't been inflated yet. We also filter out some notifications if we're on the lock
* screen and notifications for other users. So participate, see
* {@link #addPreGroupFilter} and similar methods.
* screen and notifications for other users. To participate, see
* [.addPreGroupFilter] and similar methods.
* - Grouped: Notifications that are part of the same group are clustered together into a single
* GroupEntry. These groups are then transformed in order to remove children or completely split
* them apart. To participate, see {@link #addPromoter}.
* them apart. To participate, see [.addPromoter].
* - Sorted: All top-level notifications are sorted. To participate, see
* {@link #setSections} and {@link #setComparators}
* [.setSections] and [.setComparators]
*
* The exact order of all hooks is as follows:
* 0. Collection listeners are fired ({@link #addCollectionListener}).
* 1. Pre-group filters are fired on each notification ({@link #addPreGroupFilter}).
* 0. Collection listeners are fired ([.addCollectionListener]).
* 1. Pre-group filters are fired on each notification ([.addPreGroupFilter]).
* 2. Initial grouping is performed (NotificationEntries will have their parents set
* appropriately).
* 3. OnBeforeTransformGroupListeners are fired ({@link #addOnBeforeTransformGroupsListener})
* 4. NotifPromoters are called on each notification with a parent ({@link #addPromoter})
* 5. OnBeforeSortListeners are fired ({@link #addOnBeforeSortListener})
* 6. Top-level entries are assigned sections by NotifSections ({@link #setSections})
* 7. Top-level entries within the same section are sorted by NotifComparators
* ({@link #setComparators})
* 8. Finalize filters are fired on each notification ({@link #addFinalizeFilter})
* 9. OnBeforeRenderListListeners are fired ({@link #addOnBeforeRenderListListener})
* 9. The list is handed off to the view layer to be rendered
* 3. OnBeforeTransformGroupListeners are fired ([.addOnBeforeTransformGroupsListener])
* 4. NotifPromoters are called on each notification with a parent ([.addPromoter])
* 5. Finalize filters are fired on each notification ([.addFinalizeFilter])
* 6. OnBeforeSortListeners are fired ([.addOnBeforeSortListener])
* 7. Top-level entries are assigned sections by NotifSections ([.setSections])
* 8. Top-level entries within the same section are sorted by NotifComparators ([.setComparators])
* 9. OnBeforeRenderListListeners are fired ([.addOnBeforeRenderListListener])
* 10. The list is handed off to the view layer to be rendered
*/
@SysUISingleton
public class NotifPipeline implements CommonNotifCollection {
private final NotifCollection mNotifCollection;
private final ShadeListBuilder mShadeListBuilder;
@Inject
public NotifPipeline(
NotifCollection notifCollection,
ShadeListBuilder shadeListBuilder) {
mNotifCollection = notifCollection;
mShadeListBuilder = shadeListBuilder;
}
class NotifPipeline @Inject constructor(
private val mNotifCollection: NotifCollection,
private val mShadeListBuilder: ShadeListBuilder
) : CommonNotifCollection {
/**
* Returns the list of all known notifications, i.e. the notifications that are currently posted
* to the phone. In general, this tracks closely to the list maintained by NotificationManager,
@@ -93,39 +77,35 @@ public class NotifPipeline implements CommonNotifCollection {
*
* The returned collection is read-only, unsorted, unfiltered, and ungrouped.
*/
@Override
public Collection<NotificationEntry> getAllNotifs() {
return mNotifCollection.getAllNotifs();
override fun getAllNotifs(): Collection<NotificationEntry> {
return mNotifCollection.allNotifs
}
@Override
public void addCollectionListener(NotifCollectionListener listener) {
mNotifCollection.addCollectionListener(listener);
override fun addCollectionListener(listener: NotifCollectionListener) {
mNotifCollection.addCollectionListener(listener)
}
/**
* Returns the NotificationEntry associated with [key].
*/
@Override
@Nullable
public NotificationEntry getEntry(String key) {
return mNotifCollection.getEntry(key);
override fun getEntry(key: String): NotificationEntry? {
return mNotifCollection.getEntry(key)
}
/**
* Registers a lifetime extender. Lifetime extenders can cause notifications that have been
* dismissed or retracted by system server to be temporarily retained in the collection.
*/
public void addNotificationLifetimeExtender(NotifLifetimeExtender extender) {
mNotifCollection.addNotificationLifetimeExtender(extender);
fun addNotificationLifetimeExtender(extender: NotifLifetimeExtender) {
mNotifCollection.addNotificationLifetimeExtender(extender)
}
/**
* Registers a dismiss interceptor. Dismiss interceptors can cause notifications that have been
* dismissed by the user to be retained (won't send a dismissal to system server).
*/
public void addNotificationDismissInterceptor(NotifDismissInterceptor interceptor) {
mNotifCollection.addNotificationDismissInterceptor(interceptor);
fun addNotificationDismissInterceptor(interceptor: NotifDismissInterceptor) {
mNotifCollection.addNotificationDismissInterceptor(interceptor)
}
/**
@@ -134,16 +114,16 @@ public class NotifPipeline implements CommonNotifCollection {
* returns true, the notification is removed from the pipeline (and no other filters are
* called on that notif).
*/
public void addPreGroupFilter(NotifFilter filter) {
mShadeListBuilder.addPreGroupFilter(filter);
fun addPreGroupFilter(filter: NotifFilter) {
mShadeListBuilder.addPreGroupFilter(filter)
}
/**
* Called after notifications have been filtered and after the initial grouping has been
* performed but before NotifPromoters have had a chance to promote children out of groups.
*/
public void addOnBeforeTransformGroupsListener(OnBeforeTransformGroupsListener listener) {
mShadeListBuilder.addOnBeforeTransformGroupsListener(listener);
fun addOnBeforeTransformGroupsListener(listener: OnBeforeTransformGroupsListener) {
mShadeListBuilder.addOnBeforeTransformGroupsListener(listener)
}
/**
@@ -153,34 +133,34 @@ public class NotifPipeline implements CommonNotifCollection {
* registered. If any promoter returns true, the notification is removed from the group (and no
* other promoters are called on it).
*/
public void addPromoter(NotifPromoter promoter) {
mShadeListBuilder.addPromoter(promoter);
fun addPromoter(promoter: NotifPromoter) {
mShadeListBuilder.addPromoter(promoter)
}
/**
* Called after notifs have been filtered and groups have been determined but before sections
* have been determined or the notifs have been sorted.
*/
public void addOnBeforeSortListener(OnBeforeSortListener listener) {
mShadeListBuilder.addOnBeforeSortListener(listener);
fun addOnBeforeSortListener(listener: OnBeforeSortListener) {
mShadeListBuilder.addOnBeforeSortListener(listener)
}
/**
* Sections that are used to sort top-level entries. If two entries have the same section,
* NotifComparators are consulted. Sections from this list are called in order for each
* notification passed through the pipeline. The first NotifSection to return true for
* {@link NotifSectioner#isInSection(ListEntry)} sets the entry as part of its Section.
* [NotifSectioner.isInSection] sets the entry as part of its Section.
*/
public void setSections(List<NotifSectioner> sections) {
mShadeListBuilder.setSectioners(sections);
fun setSections(sections: List<NotifSectioner>) {
mShadeListBuilder.setSectioners(sections)
}
/**
* StabilityManager that is used to determine whether to suppress group and section changes.
* This should only be set once.
*/
public void setVisualStabilityManager(NotifStabilityManager notifStabilityManager) {
mShadeListBuilder.setNotifStabilityManager(notifStabilityManager);
fun setVisualStabilityManager(notifStabilityManager: NotifStabilityManager) {
mShadeListBuilder.setNotifStabilityManager(notifStabilityManager)
}
/**
@@ -188,16 +168,16 @@ public class NotifPipeline implements CommonNotifCollection {
* comparators are executed in order until one of them returns a non-zero result. If all return
* zero, the pipeline falls back to sorting by rank (and, failing that, Notification.when).
*/
public void setComparators(List<NotifComparator> comparators) {
mShadeListBuilder.setComparators(comparators);
fun setComparators(comparators: List<NotifComparator>) {
mShadeListBuilder.setComparators(comparators)
}
/**
* Called after notifs have been filtered once, grouped, and sorted but before the final
* filtering.
*/
public void addOnBeforeFinalizeFilterListener(OnBeforeFinalizeFilterListener listener) {
mShadeListBuilder.addOnBeforeFinalizeFilterListener(listener);
fun addOnBeforeFinalizeFilterListener(listener: OnBeforeFinalizeFilterListener) {
mShadeListBuilder.addOnBeforeFinalizeFilterListener(listener)
}
/**
@@ -207,21 +187,21 @@ public class NotifPipeline implements CommonNotifCollection {
* true, the notification is removed from the pipeline (and no other filters are called on that
* notif).
*/
public void addFinalizeFilter(NotifFilter filter) {
mShadeListBuilder.addFinalizeFilter(filter);
fun addFinalizeFilter(filter: NotifFilter) {
mShadeListBuilder.addFinalizeFilter(filter)
}
/**
* Called at the end of the pipeline after the notif list has been finalized but before it has
* been handed off to the view layer.
*/
public void addOnBeforeRenderListListener(OnBeforeRenderListListener listener) {
mShadeListBuilder.addOnBeforeRenderListListener(listener);
fun addOnBeforeRenderListListener(listener: OnBeforeRenderListListener) {
mShadeListBuilder.addOnBeforeRenderListListener(listener)
}
/** Registers an invalidator that can be used to invalidate the entire notif list. */
public void addPreRenderInvalidator(Invalidator invalidator) {
mShadeListBuilder.addPreRenderInvalidator(invalidator);
fun addPreRenderInvalidator(invalidator: Invalidator) {
mShadeListBuilder.addPreRenderInvalidator(invalidator)
}
/**
@@ -231,8 +211,8 @@ public class NotifPipeline implements CommonNotifCollection {
* @param name the name of the component that will update notifiations
* @return an updater
*/
public InternalNotifUpdater getInternalNotifUpdater(String name) {
return mNotifCollection.getInternalNotifUpdater(name);
fun getInternalNotifUpdater(name: String?): InternalNotifUpdater {
return mNotifCollection.getInternalNotifUpdater(name)
}
/**
@@ -240,8 +220,20 @@ public class NotifPipeline implements CommonNotifCollection {
* are currently present in the shade. If this method is called during pipeline execution it
* will return the current state of the list, which will likely be only partially-generated.
*/
public List<ListEntry> getShadeList() {
return mShadeListBuilder.getShadeList();
val shadeList: List<ListEntry>
get() = mShadeListBuilder.shadeList
/**
* Constructs a flattened representation of the notification tree, where each group will have
* the summary (if present) followed by the children.
*/
fun getFlatShadeList(): List<NotificationEntry> = shadeList.flatMap { entry ->
when (entry) {
is NotificationEntry -> sequenceOf(entry)
is GroupEntry -> (entry.summary?.let { sequenceOf(it) }.orEmpty() +
entry.children)
else -> throw RuntimeException("Unexpected entry $entry")
}
}
/**
@@ -250,20 +242,9 @@ public class NotifPipeline implements CommonNotifCollection {
* will return the number of notifications in its current state, which will likely be only
* partially-generated.
*/
public int getShadeListCount() {
final List<ListEntry> entries = getShadeList();
int numNotifs = 0;
for (int i = 0; i < entries.size(); i++) {
final ListEntry entry = entries.get(i);
if (entry instanceof GroupEntry) {
final GroupEntry parentEntry = (GroupEntry) entry;
numNotifs++; // include the summary in the count
numNotifs += parentEntry.getChildren().size();
} else {
numNotifs++;
}
}
return numNotifs;
fun getShadeListCount(): Int = shadeList.sumOf { entry ->
// include the summary in the count
if (entry is GroupEntry) 1 + entry.children.size
else 1
}
}
}

View File

@@ -214,6 +214,7 @@ public interface NotificationsModule {
FeatureFlags featureFlags,
NotificationVisibilityProvider visibilityProvider,
NotificationEntryManager entryManager,
NotifPipeline notifPipeline,
StatusBarStateController statusBarStateController,
NotificationLogger.ExpansionStateLogger expansionStateLogger,
NotificationPanelLogger notificationPanelLogger) {
@@ -223,6 +224,7 @@ public interface NotificationsModule {
featureFlags,
visibilityProvider,
entryManager,
notifPipeline,
statusBarStateController,
expansionStateLogger,
notificationPanelLogger);

View File

@@ -26,6 +26,7 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.GuardedBy;
@@ -40,7 +41,9 @@ import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.dagger.NotificationsModule;
import com.android.systemui.statusbar.notification.stack.ExpandableViewState;
@@ -75,6 +78,7 @@ public class NotificationLogger implements StateListener {
private final FeatureFlags mFeatureFlags;
private final NotificationVisibilityProvider mVisibilityProvider;
private final NotificationEntryManager mEntryManager;
private final NotifPipeline mNotifPipeline;
private final NotificationPanelLogger mNotificationPanelLogger;
private final ExpansionStateLogger mExpansionStateLogger;
@@ -131,9 +135,7 @@ public class NotificationLogger implements StateListener {
// notifications.
// 3. Report newly visible and no-longer visible notifications.
// 4. Keep currently visible notifications for next report.
// TODO(b/204764064): support new pipeline
mFeatureFlags.checkLegacyPipelineEnabled();
List<NotificationEntry> activeNotifications = mEntryManager.getVisibleNotifications();
List<NotificationEntry> activeNotifications = getVisibleNotifications();
int N = activeNotifications.size();
for (int i = 0; i < N; i++) {
NotificationEntry entry = activeNotifications.get(i);
@@ -172,6 +174,14 @@ public class NotificationLogger implements StateListener {
}
};
private List<NotificationEntry> getVisibleNotifications() {
if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
return mNotifPipeline.getFlatShadeList();
} else {
return mEntryManager.getVisibleNotifications();
}
}
/**
* Returns the location of the notification referenced by the given {@link NotificationEntry}.
*/
@@ -211,6 +221,7 @@ public class NotificationLogger implements StateListener {
FeatureFlags featureFlags,
NotificationVisibilityProvider visibilityProvider,
NotificationEntryManager entryManager,
NotifPipeline notifPipeline,
StatusBarStateController statusBarStateController,
ExpansionStateLogger expansionStateLogger,
NotificationPanelLogger notificationPanelLogger) {
@@ -219,6 +230,7 @@ public class NotificationLogger implements StateListener {
mFeatureFlags = featureFlags;
mVisibilityProvider = visibilityProvider;
mEntryManager = entryManager;
mNotifPipeline = notifPipeline;
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
mExpansionStateLogger = expansionStateLogger;
@@ -226,7 +238,15 @@ public class NotificationLogger implements StateListener {
// Not expected to be destroyed, don't need to unsubscribe
statusBarStateController.addCallback(this);
entryManager.addNotificationEntryListener(new NotificationEntryListener() {
if (mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
registerNewPipelineListener();
} else {
registerLegacyListener();
}
}
private void registerLegacyListener() {
mEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
@Override
public void onEntryRemoved(
NotificationEntry entry,
@@ -250,6 +270,20 @@ public class NotificationLogger implements StateListener {
});
}
private void registerNewPipelineListener() {
mNotifPipeline.addCollectionListener(new NotifCollectionListener() {
@Override
public void onEntryUpdated(@NonNull NotificationEntry entry, boolean fromSystem) {
mExpansionStateLogger.onEntryUpdated(entry.getKey());
}
@Override
public void onEntryRemoved(@NonNull NotificationEntry entry, int reason) {
mExpansionStateLogger.onEntryRemoved(entry.getKey());
}
});
}
public void setUpWithContainer(NotificationListContainer listContainer) {
mListContainer = listContainer;
}
@@ -417,10 +451,7 @@ public class NotificationLogger implements StateListener {
// Once we know panelExpanded and Dozing, turn logging on & off when appropriate
boolean lockscreen = mLockscreen == null ? false : mLockscreen;
if (mPanelExpanded && !mDozing) {
// TODO(b/204764064): support new pipeline
mFeatureFlags.checkLegacyPipelineEnabled();
mNotificationPanelLogger.logPanelShown(lockscreen,
mEntryManager.getVisibleNotifications());
mNotificationPanelLogger.logPanelShown(lockscreen, getVisibleNotifications());
if (DEBUG) {
Log.i(TAG, "Notification panel shown, lockscreen=" + lockscreen);
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright (C) 2019 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.collection
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when` as whenever
@SmallTest
@RunWith(AndroidTestingRunner::class)
class NotifPipelineTest : SysuiTestCase() {
@Mock private lateinit var notifCollection: NotifCollection
@Mock private lateinit var shadeListBuilder: ShadeListBuilder
private lateinit var notifPipeline: NotifPipeline
@Before
fun setup() {
MockitoAnnotations.initMocks(this)
notifPipeline = NotifPipeline(notifCollection, shadeListBuilder)
whenever(shadeListBuilder.shadeList).thenReturn(listOf(
NotificationEntryBuilder().setPkg("foo").setId(1).build(),
NotificationEntryBuilder().setPkg("foo").setId(2).build(),
group(
NotificationEntryBuilder().setPkg("bar").setId(1).build(),
NotificationEntryBuilder().setPkg("bar").setId(2).build(),
NotificationEntryBuilder().setPkg("bar").setId(3).build(),
NotificationEntryBuilder().setPkg("bar").setId(4).build()
),
NotificationEntryBuilder().setPkg("baz").setId(1).build()
))
}
private fun group(summary: NotificationEntry, vararg children: NotificationEntry): GroupEntry {
return GroupEntry(summary.key, summary.creationTime).also { group ->
group.summary = summary
for (it in children) {
group.addChild(it)
}
}
}
@Test
fun testGetShadeListCount() {
assertThat(notifPipeline.getShadeListCount()).isEqualTo(7)
}
@Test
fun testGetFlatShadeList() {
assertThat(notifPipeline.getFlatShadeList().map { it.key }).containsExactly(
"0|foo|1|null|0",
"0|foo|2|null|0",
"0|bar|1|null|0",
"0|bar|2|null|0",
"0|bar|3|null|0",
"0|bar|4|null|0",
"0|baz|1|null|0"
).inOrder()
}
}

View File

@@ -29,12 +29,12 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga
import com.android.systemui.statusbar.notification.collection.render.NodeController
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier
import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier.Companion.TYPE_PERSON
import com.android.systemui.util.mockito.withArgCaptor
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
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.verify
import org.mockito.MockitoAnnotations
@@ -65,9 +65,9 @@ class ConversationCoordinatorTest : SysuiTestCase() {
coordinator.attach(pipeline)
// capture arguments:
val notifPromoterCaptor = ArgumentCaptor.forClass(NotifPromoter::class.java)
verify(pipeline).addPromoter(notifPromoterCaptor.capture())
promoter = notifPromoterCaptor.value
promoter = withArgCaptor {
verify(pipeline).addPromoter(capture())
}
peopleSectioner = coordinator.sectioner

View File

@@ -28,7 +28,7 @@ import com.android.systemui.statusbar.notification.collection.notifcollection.No
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewListener
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager
import com.android.systemui.statusbar.notification.row.NotificationGuts
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.withArgCaptor
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
@@ -60,13 +60,11 @@ class GutsCoordinatorTest : SysuiTestCase() {
initMocks(this)
coordinator = GutsCoordinator(notifGutsViewManager, logger, dumpManager)
coordinator.attach(pipeline)
notifLifetimeExtender = argumentCaptor<NotifLifetimeExtender>().let {
verify(pipeline).addNotificationLifetimeExtender(it.capture())
it.value!!
notifLifetimeExtender = withArgCaptor {
verify(pipeline).addNotificationLifetimeExtender(capture())
}
notifGutsViewListener = argumentCaptor<NotifGutsViewListener>().let {
verify(notifGutsViewManager).setGutsListener(it.capture())
it.value!!
notifGutsViewListener = withArgCaptor {
verify(notifGutsViewManager).setGutsListener(capture())
}
notifLifetimeExtender.setCallback(lifetimeExtenderCallback)
entry1 = NotificationEntryBuilder().setId(1).build()

View File

@@ -26,7 +26,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
import com.android.systemui.statusbar.notification.collection.listbuilder.OnBeforeRenderListListener
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.withArgCaptor
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -56,13 +56,11 @@ class ShadeEventCoordinatorTest : SysuiTestCase() {
initMocks(this)
coordinator = ShadeEventCoordinator(logger)
coordinator.attach(pipeline)
notifCollectionListener = argumentCaptor<NotifCollectionListener>().let {
verify(pipeline).addCollectionListener(it.capture())
it.value!!
notifCollectionListener = withArgCaptor {
verify(pipeline).addCollectionListener(capture())
}
onBeforeRenderListListener = argumentCaptor<OnBeforeRenderListListener>().let {
verify(pipeline).addOnBeforeRenderListListener(it.capture())
it.value!!
onBeforeRenderListListener = withArgCaptor {
verify(pipeline).addOnBeforeRenderListListener(capture())
}
coordinator.setNotifRemovedByUserCallback(notifRemovedByUserCallback)
coordinator.setShadeEmptiedCallback(shadeEmptiedCallback)

View File

@@ -35,15 +35,13 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.mockito.capture
import com.android.systemui.util.mockito.withArgCaptor
import com.android.systemui.util.time.FakeSystemClock
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.`when`
import org.mockito.Mockito.anyString
@@ -69,15 +67,6 @@ class SmartspaceDedupingCoordinatorTest : SysuiTestCase() {
@Mock
private lateinit var pluggableListener: Pluggable.PluggableListener<NotifFilter>
@Captor
private lateinit var filterCaptor: ArgumentCaptor<NotifFilter>
@Captor
private lateinit var collectionListenerCaptor: ArgumentCaptor<NotifCollectionListener>
@Captor
private lateinit var stateListenerCaptor: ArgumentCaptor<StatusBarStateController.StateListener>
@Captor
private lateinit var smartspaceListenerCaptor: ArgumentCaptor<SmartspaceTargetListener>
private lateinit var filter: NotifFilter
private lateinit var collectionListener: NotifCollectionListener
private lateinit var statusBarListener: StatusBarStateController.StateListener
@@ -118,18 +107,22 @@ class SmartspaceDedupingCoordinatorTest : SysuiTestCase() {
// Attach the deduper and capture the listeners/filters that it registers
deduper.attach(notifPipeline)
verify(notifPipeline).addPreGroupFilter(filterCaptor.capture())
filter = filterCaptor.value
filter = withArgCaptor {
verify(notifPipeline).addPreGroupFilter(capture())
}
filter.setInvalidationListener(pluggableListener)
verify(notifPipeline).addCollectionListener(capture(collectionListenerCaptor))
collectionListener = collectionListenerCaptor.value
collectionListener = withArgCaptor {
verify(notifPipeline).addCollectionListener(capture())
}
verify(statusBarStateController).addCallback(capture(stateListenerCaptor))
statusBarListener = stateListenerCaptor.value
statusBarListener = withArgCaptor {
verify(statusBarStateController).addCallback(capture())
}
verify(smartspaceController).addListener(capture(smartspaceListenerCaptor))
newTargetListener = smartspaceListenerCaptor.value
newTargetListener = withArgCaptor {
verify(smartspaceController).addListener(capture())
}
// Initialize some test data
entry1HasRecentlyAlerted = NotificationEntryBuilder()

View File

@@ -0,0 +1,283 @@
/*
* Copyright (C) 2017 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.logging;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
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.when;
import android.app.Notification;
import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import androidx.test.filters.SmallTest;
import com.android.internal.logging.InstanceId;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
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.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.logging.nano.Notifications;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
import com.google.android.collect.Lists;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class NotificationLoggerLegacyTest extends SysuiTestCase {
private static final String TEST_PACKAGE_NAME = "test";
private static final int TEST_UID = 0;
@Mock private NotificationListContainer mListContainer;
@Mock private IStatusBarService mBarService;
@Mock private ExpandableNotificationRow mRow;
@Mock private NotificationLogger.ExpansionStateLogger mExpansionStateLogger;
// Dependency mocks:
@Mock private FeatureFlags mFeatureFlags;
@Mock private NotificationVisibilityProvider mVisibilityProvider;
@Mock private NotificationEntryManager mEntryManager;
@Mock private NotifPipeline mNotifPipeline;
@Mock private NotificationListener mListener;
private NotificationEntry mEntry;
private TestableNotificationLogger mLogger;
private ConcurrentLinkedQueue<AssertionError> mErrorQueue = new ConcurrentLinkedQueue<>();
private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
private NotificationPanelLoggerFake mNotificationPanelLoggerFake =
new NotificationPanelLoggerFake();
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mEntry = new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_NAME)
.setOpPkg(TEST_PACKAGE_NAME)
.setUid(TEST_UID)
.setNotification(new Notification())
.setUser(UserHandle.CURRENT)
.setInstanceId(InstanceId.fakeInstanceId(1))
.build();
mEntry.setRow(mRow);
mLogger = new TestableNotificationLogger(
mListener,
mUiBgExecutor,
mFeatureFlags,
mVisibilityProvider,
mEntryManager,
mNotifPipeline,
mock(StatusBarStateControllerImpl.class),
mBarService,
mExpansionStateLogger
);
mLogger.setUpWithContainer(mListContainer);
verify(mEntryManager).addNotificationEntryListener(any());
verify(mNotifPipeline, never()).addCollectionListener(any());
}
@Test
public void testOnChildLocationsChangedReportsVisibilityChanged() throws Exception {
NotificationVisibility[] newlyVisibleKeys = {
NotificationVisibility.obtain(mEntry.getKey(), 0, 1, true)
};
NotificationVisibility[] noLongerVisibleKeys = {};
doAnswer(invocation -> {
try {
assertArrayEquals(newlyVisibleKeys,
(NotificationVisibility[]) invocation.getArguments()[0]);
assertArrayEquals(noLongerVisibleKeys,
(NotificationVisibility[]) invocation.getArguments()[1]);
} catch (AssertionError error) {
mErrorQueue.offer(error);
}
return null;
}
).when(mBarService).onNotificationVisibilityChanged(any(NotificationVisibility[].class),
any(NotificationVisibility[].class));
when(mListContainer.isInVisibleLocation(any())).thenReturn(true);
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(mEntry));
mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
TestableLooper.get(this).processAllMessages();
mUiBgExecutor.runAllReady();
if (!mErrorQueue.isEmpty()) {
throw mErrorQueue.poll();
}
// |mEntry| won't change visibility, so it shouldn't be reported again:
Mockito.reset(mBarService);
mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
TestableLooper.get(this).processAllMessages();
mUiBgExecutor.runAllReady();
verify(mBarService, never()).onNotificationVisibilityChanged(any(), any());
}
@Test
public void testStoppingNotificationLoggingReportsCurrentNotifications()
throws Exception {
when(mListContainer.isInVisibleLocation(any())).thenReturn(true);
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(mEntry));
mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
TestableLooper.get(this).processAllMessages();
mUiBgExecutor.runAllReady();
Mockito.reset(mBarService);
setStateAsleep();
mLogger.onDozingChanged(false); // Wake to lockscreen
mLogger.onDozingChanged(true); // And go back to sleep, turning off logging
mUiBgExecutor.runAllReady();
// The visibility objects are recycled by NotificationLogger, so we can't use specific
// matchers here.
verify(mBarService, times(1)).onNotificationVisibilityChanged(any(), any());
}
private void setStateAsleep() {
mLogger.onPanelExpandedChanged(true);
mLogger.onDozingChanged(true);
mLogger.onStateChanged(StatusBarState.KEYGUARD);
}
private void setStateAwake() {
mLogger.onPanelExpandedChanged(false);
mLogger.onDozingChanged(false);
mLogger.onStateChanged(StatusBarState.SHADE);
}
@Test
public void testLogPanelShownOnWake() {
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(mEntry));
setStateAsleep();
mLogger.onDozingChanged(false); // Wake to lockscreen
assertEquals(1, mNotificationPanelLoggerFake.getCalls().size());
assertTrue(mNotificationPanelLoggerFake.get(0).isLockscreen);
assertEquals(1, mNotificationPanelLoggerFake.get(0).list.notifications.length);
Notifications.Notification n = mNotificationPanelLoggerFake.get(0).list.notifications[0];
assertEquals(TEST_PACKAGE_NAME, n.packageName);
assertEquals(TEST_UID, n.uid);
assertEquals(1, n.instanceId);
assertFalse(n.isGroupSummary);
assertEquals(Notifications.Notification.SECTION_ALERTING, n.section);
}
@Test
public void testLogPanelShownOnShadePull() {
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(mEntry));
setStateAwake();
// Now expand panel
mLogger.onPanelExpandedChanged(true);
assertEquals(1, mNotificationPanelLoggerFake.getCalls().size());
assertFalse(mNotificationPanelLoggerFake.get(0).isLockscreen);
assertEquals(1, mNotificationPanelLoggerFake.get(0).list.notifications.length);
Notifications.Notification n = mNotificationPanelLoggerFake.get(0).list.notifications[0];
assertEquals(TEST_PACKAGE_NAME, n.packageName);
assertEquals(TEST_UID, n.uid);
assertEquals(1, n.instanceId);
assertFalse(n.isGroupSummary);
assertEquals(Notifications.Notification.SECTION_ALERTING, n.section);
}
@Test
public void testLogPanelShownHandlesNullInstanceIds() {
// Construct a NotificationEntry like mEntry, but with a null instance id.
NotificationEntry entry = new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_NAME)
.setOpPkg(TEST_PACKAGE_NAME)
.setUid(TEST_UID)
.setNotification(new Notification())
.setUser(UserHandle.CURRENT)
.build();
entry.setRow(mRow);
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(entry));
setStateAsleep();
mLogger.onDozingChanged(false); // Wake to lockscreen
assertEquals(1, mNotificationPanelLoggerFake.getCalls().size());
assertEquals(1, mNotificationPanelLoggerFake.get(0).list.notifications.length);
Notifications.Notification n = mNotificationPanelLoggerFake.get(0).list.notifications[0];
assertEquals(0, n.instanceId);
}
private class TestableNotificationLogger extends NotificationLogger {
TestableNotificationLogger(NotificationListener notificationListener,
Executor uiBgExecutor,
FeatureFlags featureFlags,
NotificationVisibilityProvider visibilityProvider,
NotificationEntryManager entryManager,
NotifPipeline notifPipeline,
StatusBarStateControllerImpl statusBarStateController,
IStatusBarService barService,
ExpansionStateLogger expansionStateLogger) {
super(
notificationListener,
uiBgExecutor,
featureFlags,
visibilityProvider,
entryManager,
notifPipeline,
statusBarStateController,
expansionStateLogger,
mNotificationPanelLoggerFake
);
mBarService = barService;
// Make this on the current thread so we can wait for it during tests.
mHandler = Handler.createAsync(Looper.myLooper());
}
OnChildLocationsChangedListener getChildLocationsChangedListenerForTest() {
return mNotificationLocationsChangedListener;
}
}
}

View File

@@ -45,8 +45,8 @@ import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.StatusBarState;
import com.android.systemui.statusbar.StatusBarStateControllerImpl;
import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
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.render.NotificationVisibilityProvider;
@@ -61,8 +61,6 @@ import com.google.android.collect.Lists;
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.Mockito;
import org.mockito.MockitoAnnotations;
@@ -86,8 +84,8 @@ public class NotificationLoggerTest extends SysuiTestCase {
@Mock private FeatureFlags mFeatureFlags;
@Mock private NotificationVisibilityProvider mVisibilityProvider;
@Mock private NotificationEntryManager mEntryManager;
@Mock private NotifPipeline mNotifPipeline;
@Mock private NotificationListener mListener;
@Captor private ArgumentCaptor<NotificationEntryListener> mEntryListenerCaptor;
private NotificationEntry mEntry;
private TestableNotificationLogger mLogger;
@@ -99,8 +97,7 @@ public class NotificationLoggerTest extends SysuiTestCase {
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
mDependency.injectTestDependency(NotificationListener.class, mListener);
when(mFeatureFlags.isNewNotifPipelineRenderingEnabled()).thenReturn(true);
mEntry = new NotificationEntryBuilder()
.setPkg(TEST_PACKAGE_NAME)
@@ -118,12 +115,14 @@ public class NotificationLoggerTest extends SysuiTestCase {
mFeatureFlags,
mVisibilityProvider,
mEntryManager,
mNotifPipeline,
mock(StatusBarStateControllerImpl.class),
mBarService,
mExpansionStateLogger
);
mLogger.setUpWithContainer(mListContainer);
verify(mEntryManager).addNotificationEntryListener(mEntryListenerCaptor.capture());
verify(mEntryManager, never()).addNotificationEntryListener(any());
verify(mNotifPipeline).addCollectionListener(any());
}
@Test
@@ -147,12 +146,12 @@ public class NotificationLoggerTest extends SysuiTestCase {
any(NotificationVisibility[].class));
when(mListContainer.isInVisibleLocation(any())).thenReturn(true);
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(mEntry));
when(mNotifPipeline.getFlatShadeList()).thenReturn(Lists.newArrayList(mEntry));
mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
TestableLooper.get(this).processAllMessages();
mUiBgExecutor.runAllReady();
if(!mErrorQueue.isEmpty()) {
if (!mErrorQueue.isEmpty()) {
throw mErrorQueue.poll();
}
@@ -169,7 +168,7 @@ public class NotificationLoggerTest extends SysuiTestCase {
public void testStoppingNotificationLoggingReportsCurrentNotifications()
throws Exception {
when(mListContainer.isInVisibleLocation(any())).thenReturn(true);
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(mEntry));
when(mNotifPipeline.getFlatShadeList()).thenReturn(Lists.newArrayList(mEntry));
mLogger.getChildLocationsChangedListenerForTest().onChildLocationsChanged();
TestableLooper.get(this).processAllMessages();
mUiBgExecutor.runAllReady();
@@ -198,7 +197,7 @@ public class NotificationLoggerTest extends SysuiTestCase {
@Test
public void testLogPanelShownOnWake() {
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(mEntry));
when(mNotifPipeline.getFlatShadeList()).thenReturn(Lists.newArrayList(mEntry));
setStateAsleep();
mLogger.onDozingChanged(false); // Wake to lockscreen
assertEquals(1, mNotificationPanelLoggerFake.getCalls().size());
@@ -214,7 +213,7 @@ public class NotificationLoggerTest extends SysuiTestCase {
@Test
public void testLogPanelShownOnShadePull() {
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(mEntry));
when(mNotifPipeline.getFlatShadeList()).thenReturn(Lists.newArrayList(mEntry));
setStateAwake();
// Now expand panel
mLogger.onPanelExpandedChanged(true);
@@ -242,7 +241,7 @@ public class NotificationLoggerTest extends SysuiTestCase {
.build();
entry.setRow(mRow);
when(mEntryManager.getVisibleNotifications()).thenReturn(Lists.newArrayList(entry));
when(mNotifPipeline.getFlatShadeList()).thenReturn(Lists.newArrayList(entry));
setStateAsleep();
mLogger.onDozingChanged(false); // Wake to lockscreen
assertEquals(1, mNotificationPanelLoggerFake.getCalls().size());
@@ -258,6 +257,7 @@ public class NotificationLoggerTest extends SysuiTestCase {
FeatureFlags featureFlags,
NotificationVisibilityProvider visibilityProvider,
NotificationEntryManager entryManager,
NotifPipeline notifPipeline,
StatusBarStateControllerImpl statusBarStateController,
IStatusBarService barService,
ExpansionStateLogger expansionStateLogger) {
@@ -267,6 +267,7 @@ public class NotificationLoggerTest extends SysuiTestCase {
featureFlags,
visibilityProvider,
entryManager,
notifPipeline,
statusBarStateController,
expansionStateLogger,
mNotificationPanelLoggerFake
@@ -276,8 +277,7 @@ public class NotificationLoggerTest extends SysuiTestCase {
mHandler = Handler.createAsync(Looper.myLooper());
}
OnChildLocationsChangedListener
getChildLocationsChangedListenerForTest() {
OnChildLocationsChangedListener getChildLocationsChangedListenerForTest() {
return mNotificationLocationsChangedListener;
}
}

View File

@@ -117,6 +117,7 @@ import com.android.systemui.statusbar.notification.DynamicPrivacyController;
import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.NotificationFilter;
import com.android.systemui.statusbar.notification.NotificationWakeUpCoordinator;
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.legacy.VisualStabilityManager;
@@ -317,6 +318,7 @@ public class StatusBarTest extends SysuiTestCase {
mFeatureFlags,
mVisibilityProvider,
mock(NotificationEntryManager.class),
mock(NotifPipeline.class),
mStatusBarStateController,
mExpansionStateLogger,
new NotificationPanelLoggerFake()

View File

@@ -66,6 +66,27 @@ inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
*/
inline fun <reified T : Any> mock(): T = Mockito.mock(T::class.java)
/**
* A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
* kotlin tests are mocking kotlin objects and the methods take non-null parameters:
*
* java.lang.NullPointerException: capture() must not be null
*/
class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) {
private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz)
fun capture(): T = wrapped.capture()
val value: T
get() = wrapped.value
}
/**
* Helper function for creating an argumentCaptor in kotlin.
*
* Generic T is nullable because implicitly bounded by Any?.
*/
inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> =
KotlinArgumentCaptor(T::class.java)
/**
* Helper function for creating and using a single-use ArgumentCaptor in kotlin.
*
@@ -76,6 +97,8 @@ inline fun <reified T : Any> mock(): T = Mockito.mock(T::class.java)
* becomes:
*
* val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) }
*
* NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException.
*/
inline fun <reified T : Any> withArgCaptor(block: ArgumentCaptor<T>.() -> Unit): T =
argumentCaptor<T>().apply { block() }.value
inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T =
kotlinArgumentCaptor<T>().apply { block() }.value