Merge "Extend lifetime of notifs during activity launch" into tm-dev

This commit is contained in:
TreeHugger Robot
2022-03-04 20:43:23 +00:00
committed by Android (Google) Code Review
19 changed files with 580 additions and 204 deletions

View File

@@ -0,0 +1,95 @@
/*
* Copyright (C) 2022 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.coordinator
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender.OnEndLifetimeExtensionCallback
import com.android.systemui.statusbar.phone.NotifActivityLaunchEvents
import dagger.Binds
import dagger.Module
import javax.inject.Inject
/** Extends the lifetime of notifications while their activity launch animation is playing. */
interface ActivityLaunchAnimCoordinator : Coordinator
/** Provides an [ActivityLaunchAnimCoordinator] to [CoordinatorScope]. */
@Module(includes = [PrivateActivityStarterCoordinatorModule::class])
object ActivityLaunchAnimCoordinatorModule
@Module
private interface PrivateActivityStarterCoordinatorModule {
@Binds
fun bindCoordinator(impl: ActivityLaunchAnimCoordinatorImpl): ActivityLaunchAnimCoordinator
}
/**
* Listens for [NotifActivityLaunchEvents], and then extends the lifetimes of any notifs while their
* launch animation is playing.
*/
@CoordinatorScope
private class ActivityLaunchAnimCoordinatorImpl @Inject constructor(
private val activityLaunchEvents: NotifActivityLaunchEvents
) : ActivityLaunchAnimCoordinator {
// Tracks notification launches, and whether or not their lifetimes are extended.
private val notifsLaunchingActivities = mutableMapOf<String, Boolean>()
private var onEndLifetimeExtensionCallback: OnEndLifetimeExtensionCallback? = null
override fun attach(pipeline: NotifPipeline) {
activityLaunchEvents.registerListener(activityStartEventListener)
pipeline.addNotificationLifetimeExtender(extender)
}
private val activityStartEventListener = object : NotifActivityLaunchEvents.Listener {
override fun onStartLaunchNotifActivity(entry: NotificationEntry) {
notifsLaunchingActivities[entry.key] = false
}
override fun onFinishLaunchNotifActivity(entry: NotificationEntry) {
if (notifsLaunchingActivities.remove(entry.key) == true) {
// If we were extending the lifetime of this notification, stop.
onEndLifetimeExtensionCallback?.onEndLifetimeExtension(extender, entry)
}
}
}
private val extender = object : NotifLifetimeExtender {
override fun getName(): String = "ActivityStarterCoordinator"
override fun setCallback(callback: OnEndLifetimeExtensionCallback) {
onEndLifetimeExtensionCallback = callback
}
override fun maybeExtendLifetime(entry: NotificationEntry, reason: Int): Boolean {
if (entry.key in notifsLaunchingActivities) {
// Track that we're now extending this notif
notifsLaunchingActivities[entry.key] = true
return true
}
return false
}
override fun cancelLifetimeExtension(entry: NotificationEntry) {
if (entry.key in notifsLaunchingActivities) {
notifsLaunchingActivities[entry.key] = false
}
}
}
}

View File

@@ -58,7 +58,8 @@ class NotifCoordinatorsImpl @Inject constructor(
smartspaceDedupingCoordinator: SmartspaceDedupingCoordinator,
viewConfigCoordinator: ViewConfigCoordinator,
visualStabilityCoordinator: VisualStabilityCoordinator,
sensitiveContentCoordinator: SensitiveContentCoordinator
sensitiveContentCoordinator: SensitiveContentCoordinator,
activityLaunchAnimCoordinator: ActivityLaunchAnimCoordinator
) : NotifCoordinators {
private val mCoordinators: MutableList<Coordinator> = ArrayList()
@@ -97,6 +98,7 @@ class NotifCoordinatorsImpl @Inject constructor(
mCoordinators.add(viewConfigCoordinator)
mCoordinators.add(visualStabilityCoordinator)
mCoordinators.add(sensitiveContentCoordinator)
mCoordinators.add(activityLaunchAnimCoordinator)
if (notifPipelineFlags.isSmartspaceDedupingEnabled()) {
mCoordinators.add(smartspaceDedupingCoordinator)
}
@@ -144,4 +146,4 @@ class NotifCoordinatorsImpl @Inject constructor(
companion object {
private const val TAG = "NotifCoordinators"
}
}
}

View File

@@ -34,11 +34,11 @@ import dagger.Binds
import dagger.Module
import javax.inject.Inject
@Module(includes = [PrivateModule::class])
@Module(includes = [PrivateSensitiveContentCoordinatorModule::class])
interface SensitiveContentCoordinatorModule
@Module
private interface PrivateModule {
private interface PrivateSensitiveContentCoordinatorModule {
@Binds
fun bindCoordinator(impl: SensitiveContentCoordinatorImpl): SensitiveContentCoordinator
}
@@ -121,4 +121,4 @@ private fun extractAllRepresentativeEntries(listEntry: ListEntry): Sequence<Noti
if (listEntry is GroupEntry) {
yieldAll(extractAllRepresentativeEntries(listEntry.children))
}
}
}

View File

@@ -32,7 +32,7 @@ import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.collection.render.NotifPanelEventSource;
import com.android.systemui.statusbar.phone.NotifPanelEvents;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
@@ -53,10 +53,10 @@ import javax.inject.Inject;
// TODO(b/204468557): Move to @CoordinatorScope
@SysUISingleton
public class VisualStabilityCoordinator implements Coordinator, Dumpable,
NotifPanelEventSource.Callbacks {
NotifPanelEvents.Listener {
private final DelayableExecutor mDelayableExecutor;
private final HeadsUpManager mHeadsUpManager;
private final NotifPanelEventSource mNotifPanelEventSource;
private final NotifPanelEvents mNotifPanelEvents;
private final StatusBarStateController mStatusBarStateController;
private final VisualStabilityProvider mVisualStabilityProvider;
private final WakefulnessLifecycle mWakefulnessLifecycle;
@@ -87,7 +87,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable,
DelayableExecutor delayableExecutor,
DumpManager dumpManager,
HeadsUpManager headsUpManager,
NotifPanelEventSource notifPanelEventSource,
NotifPanelEvents notifPanelEvents,
StatusBarStateController statusBarStateController,
VisualStabilityProvider visualStabilityProvider,
WakefulnessLifecycle wakefulnessLifecycle) {
@@ -96,7 +96,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable,
mWakefulnessLifecycle = wakefulnessLifecycle;
mStatusBarStateController = statusBarStateController;
mDelayableExecutor = delayableExecutor;
mNotifPanelEventSource = notifPanelEventSource;
mNotifPanelEvents = notifPanelEvents;
dumpManager.registerDumpable(this);
}
@@ -109,7 +109,7 @@ public class VisualStabilityCoordinator implements Coordinator, Dumpable,
mStatusBarStateController.addCallback(mStatusBarStateControllerListener);
mPulsing = mStatusBarStateController.isPulsing();
mNotifPanelEventSource.registerCallbacks(this);
mNotifPanelEvents.registerListener(this);
pipeline.setVisualStabilityManager(mNotifStabilityManager);
}

View File

@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.notification.collection.coordinator.dagger
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.notification.collection.coordinator.ActivityLaunchAnimCoordinatorModule
import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinators
import com.android.systemui.statusbar.notification.collection.coordinator.NotifCoordinatorsImpl
import com.android.systemui.statusbar.notification.collection.coordinator.SensitiveContentCoordinatorModule
@@ -47,7 +48,10 @@ interface CoordinatorsSubcomponent {
}
}
@Module(includes = [SensitiveContentCoordinatorModule::class])
@Module(includes = [
ActivityLaunchAnimCoordinatorModule::class,
SensitiveContentCoordinatorModule::class,
])
private abstract class InternalCoordinatorsModule {
@Binds
@Internal
@@ -62,4 +66,4 @@ private annotation class Internal
@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class CoordinatorScope
annotation class CoordinatorScope

View File

@@ -1,128 +0,0 @@
/*
* Copyright (C) 2022 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.render
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.statusbar.phone.NotificationPanelViewController
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent.CentralSurfacesScope
import com.android.systemui.util.ListenerSet
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.multibindings.IntoSet
/** Provides certain notification panel events. */
interface NotifPanelEventSource {
/** Registers callbacks to be invoked when notification panel events occur. */
fun registerCallbacks(callbacks: Callbacks)
/** Unregisters callbacks previously registered via [.registerCallbacks] */
fun unregisterCallbacks(callbacks: Callbacks)
/** Callbacks for certain notification panel events. */
interface Callbacks {
/** Invoked when the notification panel starts or stops collapsing. */
fun onPanelCollapsingChanged(isCollapsing: Boolean)
/**
* Invoked when the notification panel starts or stops launching an [android.app.Activity].
*/
fun onLaunchingActivityChanged(isLaunchingActivity: Boolean)
}
}
@Module
abstract class NotifPanelEventSourceModule {
@Binds
@SysUISingleton
abstract fun bindEventSource(manager: NotifPanelEventSourceManager): NotifPanelEventSource
@Module
companion object {
@JvmStatic
@Provides
fun provideManager(): NotifPanelEventSourceManager = NotifPanelEventSourceManagerImpl()
}
}
@Module
object StatusBarNotifPanelEventSourceModule {
@JvmStatic
@Provides
@IntoSet
@CentralSurfacesScope
fun bindStartable(
manager: NotifPanelEventSourceManager,
notifPanelController: NotificationPanelViewController
): CentralSurfacesComponent.Startable =
EventSourceStatusBarStartableImpl(manager, notifPanelController)
}
/**
* Management layer that bridges [SysUiSingleton] and [CentralSurfacesScope]. Necessary because code
* that wants to listen to [NotifPanelEventSource] lives in [SysUiSingleton], but the events
* themselves come from [NotificationPanelViewController] in [CentralSurfacesScope].
*/
interface NotifPanelEventSourceManager : NotifPanelEventSource {
var eventSource: NotifPanelEventSource?
}
private class NotifPanelEventSourceManagerImpl
: NotifPanelEventSourceManager, NotifPanelEventSource.Callbacks {
private val callbackSet = ListenerSet<NotifPanelEventSource.Callbacks>()
override var eventSource: NotifPanelEventSource? = null
set(value) {
field?.unregisterCallbacks(this)
value?.registerCallbacks(this)
field = value
}
override fun registerCallbacks(callbacks: NotifPanelEventSource.Callbacks) {
callbackSet.addIfAbsent(callbacks)
}
override fun unregisterCallbacks(callbacks: NotifPanelEventSource.Callbacks) {
callbackSet.remove(callbacks)
}
override fun onPanelCollapsingChanged(isCollapsing: Boolean) {
callbackSet.forEach { it.onPanelCollapsingChanged(isCollapsing) }
}
override fun onLaunchingActivityChanged(isLaunchingActivity: Boolean) {
callbackSet.forEach { it.onLaunchingActivityChanged(isLaunchingActivity) }
}
}
private class EventSourceStatusBarStartableImpl(
private val manager: NotifPanelEventSourceManager,
private val notifPanelController: NotificationPanelViewController
) : CentralSurfacesComponent.Startable {
override fun start() {
manager.eventSource = notifPanelController
}
override fun stop() {
manager.eventSource = null
}
}

View File

@@ -68,7 +68,6 @@ import com.android.systemui.statusbar.notification.collection.render.GroupExpans
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager;
import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl;
import com.android.systemui.statusbar.notification.collection.render.NotifGutsViewManager;
import com.android.systemui.statusbar.notification.collection.render.NotifPanelEventSourceModule;
import com.android.systemui.statusbar.notification.collection.render.NotifShadeEventSource;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.init.NotificationsController;
@@ -84,9 +83,11 @@ import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback;
import com.android.systemui.statusbar.notification.stack.NotificationSectionsManager;
import com.android.systemui.statusbar.notification.stack.StackScrollAlgorithm;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.ShadeController;
import com.android.systemui.statusbar.phone.CentralSurfaces;
import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.phone.NotifActivityLaunchEventsModule;
import com.android.systemui.statusbar.phone.NotifPanelEventsModule;
import com.android.systemui.statusbar.phone.ShadeController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.leak.LeakDetector;
import com.android.systemui.wmshell.BubblesManager;
@@ -106,8 +107,9 @@ import dagger.Provides;
*/
@Module(includes = {
CoordinatorsModule.class,
NotifActivityLaunchEventsModule.class,
NotifPipelineChoreographerModule.class,
NotifPanelEventSourceModule.class,
NotifPanelEventsModule.class,
NotificationSectionHeadersModule.class,
})
public interface NotificationsModule {

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2022 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.phone
import com.android.systemui.statusbar.notification.collection.NotificationEntry
/** Provides events about [android.app.Activity] launches from Notifications. */
interface NotifActivityLaunchEvents {
/** Registers a [Listener] to be invoked when notification activity launch events occur. */
fun registerListener(listener: Listener)
/** Unregisters a [Listener] previously registered via [registerListener] */
fun unregisterListener(listener: Listener)
/** Listener for events about [android.app.Activity] launches from Notifications. */
interface Listener {
/** Invoked when an activity has started launching from a notification. */
fun onStartLaunchNotifActivity(entry: NotificationEntry)
/** Invoked when an activity has finished launching. */
fun onFinishLaunchNotifActivity(entry: NotificationEntry)
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2022 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.phone;
import com.android.systemui.dagger.SysUISingleton;
import dagger.Binds;
import dagger.Module;
/** Provides a {@link NotifActivityLaunchEvents} in {@link SysUISingleton} scope. */
@Module
public abstract class NotifActivityLaunchEventsModule {
@Binds
abstract NotifActivityLaunchEvents bindLaunchEvents(
StatusBarNotificationActivityStarter.LaunchEventsEmitter impl);
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright (C) 2022 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.phone
/** Provides certain notification panel events. */
interface NotifPanelEvents {
/** Registers callbacks to be invoked when notification panel events occur. */
fun registerListener(listener: Listener)
/** Unregisters callbacks previously registered via [registerListener] */
fun unregisterListener(listener: Listener)
/** Callbacks for certain notification panel events. */
interface Listener {
/** Invoked when the notification panel starts or stops collapsing. */
fun onPanelCollapsingChanged(isCollapsing: Boolean)
/**
* Invoked when the notification panel starts or stops launching an [android.app.Activity].
*/
fun onLaunchingActivityChanged(isLaunchingActivity: Boolean)
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright (C) 2022 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.phone;
import com.android.systemui.dagger.SysUISingleton;
import dagger.Binds;
import dagger.Module;
/** Provides a {@link NotifPanelEvents} in {@link SysUISingleton} scope. */
@Module
public abstract class NotifPanelEventsModule {
@Binds
abstract NotifPanelEvents bindPanelEvents(
NotificationPanelViewController.PanelEventsEmitter impl);
}

View File

@@ -128,6 +128,7 @@ import com.android.systemui.communal.CommunalSourceMonitor;
import com.android.systemui.communal.CommunalStateController;
import com.android.systemui.communal.dagger.CommunalViewComponent;
import com.android.systemui.controls.dagger.ControlsComponent;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.doze.DozeLog;
@@ -176,7 +177,6 @@ import com.android.systemui.statusbar.notification.ViewGroupFadeHelper;
import com.android.systemui.statusbar.notification.collection.ListEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.legacy.NotificationGroupManagerLegacy;
import com.android.systemui.statusbar.notification.collection.render.NotifPanelEventSource;
import com.android.systemui.statusbar.notification.collection.render.ShadeViewManager;
import com.android.systemui.statusbar.notification.row.ActivatableNotificationView;
import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
@@ -223,8 +223,7 @@ import javax.inject.Inject;
import javax.inject.Provider;
@CentralSurfacesComponent.CentralSurfacesScope
public class NotificationPanelViewController extends PanelViewController
implements NotifPanelEventSource {
public class NotificationPanelViewController extends PanelViewController {
private static final boolean DEBUG = false;
@@ -335,6 +334,7 @@ public class NotificationPanelViewController extends PanelViewController
private final TapAgainViewController mTapAgainViewController;
private final SplitShadeHeaderController mSplitShadeHeaderController;
private final RecordingController mRecordingController;
private final PanelEventsEmitter mPanelEventsEmitter;
private boolean mShouldUseSplitNotificationShade;
// The bottom padding reserved for elements of the keyguard measuring notifications
private float mKeyguardNotificationBottomPadding;
@@ -664,8 +664,6 @@ public class NotificationPanelViewController extends PanelViewController
private Optional<NotificationPanelUnfoldAnimationController>
mNotificationPanelUnfoldAnimationController;
private final ListenerSet<Callbacks> mNotifEventSourceCallbacks = new ListenerSet<>();
private final NotificationListContainer mNotificationListContainer;
private View.AccessibilityDelegate mAccessibilityDelegate = new View.AccessibilityDelegate() {
@@ -796,7 +794,8 @@ public class NotificationPanelViewController extends PanelViewController
QsFrameTranslateController qsFrameTranslateController,
SysUiState sysUiState,
KeyguardUnlockAnimationController keyguardUnlockAnimationController,
NotificationListContainer notificationListContainer) {
NotificationListContainer notificationListContainer,
PanelEventsEmitter panelEventsEmitter) {
super(view,
falsingManager,
dozeLog,
@@ -868,6 +867,7 @@ public class NotificationPanelViewController extends PanelViewController
mSecureSettings = secureSettings;
mInteractionJankMonitor = interactionJankMonitor;
mSysUiState = sysUiState;
mPanelEventsEmitter = panelEventsEmitter;
pulseExpansionHandler.setPulseExpandAbortListener(() -> {
if (mQs != null) {
mQs.animateHeaderSlidingOut();
@@ -3466,9 +3466,7 @@ public class NotificationPanelViewController extends PanelViewController
boolean wasRunning = isLaunchTransitionRunning();
super.setIsLaunchAnimationRunning(running);
if (wasRunning != isLaunchTransitionRunning()) {
for (Callbacks cb : mNotifEventSourceCallbacks) {
cb.onLaunchingActivityChanged(running);
}
mPanelEventsEmitter.notifyLaunchingActivityChanged(running);
}
}
@@ -3477,9 +3475,7 @@ public class NotificationPanelViewController extends PanelViewController
boolean wasClosing = isClosing();
super.setIsClosing(isClosing);
if (wasClosing != isClosing) {
for (Callbacks cb : mNotifEventSourceCallbacks) {
cb.onPanelCollapsingChanged(isClosing);
}
mPanelEventsEmitter.notifyPanelCollapsingChanged(isClosing);
}
}
@@ -4381,16 +4377,6 @@ public class NotificationPanelViewController extends PanelViewController
.commitUpdate(mDisplayId);
}
@Override
public void registerCallbacks(Callbacks callbacks) {
mNotifEventSourceCallbacks.addIfAbsent(callbacks);
}
@Override
public void unregisterCallbacks(Callbacks callbacks) {
mNotifEventSourceCallbacks.remove(callbacks);
}
private class OnHeightChangedListener implements ExpandableView.OnHeightChangedListener {
@Override
public void onHeightChanged(ExpandableView view, boolean needsAnimation) {
@@ -5164,4 +5150,35 @@ public class NotificationPanelViewController extends PanelViewController
1.0f /* speedUpFactor */);
}
}
@SysUISingleton
static class PanelEventsEmitter implements NotifPanelEvents {
private final ListenerSet<Listener> mListeners = new ListenerSet<>();
@Inject
PanelEventsEmitter() {}
@Override
public void registerListener(@NonNull Listener listener) {
mListeners.addIfAbsent(listener);
}
@Override
public void unregisterListener(@NonNull Listener listener) {
mListeners.remove(listener);
}
private void notifyLaunchingActivityChanged(boolean isLaunchingActivity) {
for (Listener cb : mListeners) {
cb.onLaunchingActivityChanged(isLaunchingActivity);
}
}
private void notifyPanelCollapsingChanged(boolean isCollapsing) {
for (NotifPanelEvents.Listener cb : mListeners) {
cb.onPanelCollapsingChanged(isCollapsing);
}
}
}
}

View File

@@ -41,6 +41,7 @@ import android.text.TextUtils;
import android.util.EventLog;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.jank.InteractionJankMonitor;
@@ -51,6 +52,7 @@ import com.android.systemui.ActivityIntentHelper;
import com.android.systemui.EventLogTags;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
@@ -75,6 +77,7 @@ import com.android.systemui.statusbar.notification.row.OnUserInteractionCallback
import com.android.systemui.statusbar.phone.dagger.CentralSurfacesComponent;
import com.android.systemui.statusbar.policy.HeadsUpUtil;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.util.ListenerSet;
import com.android.systemui.wmshell.BubblesManager;
import java.util.Optional;
@@ -128,6 +131,7 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte
private final ActivityLaunchAnimator mActivityLaunchAnimator;
private final NotificationLaunchAnimatorControllerProvider mNotificationAnimationProvider;
private final OnUserInteractionCallback mOnUserInteractionCallback;
private final LaunchEventsEmitter mLaunchEventsEmitter;
private boolean mIsCollapsingToShowActivityOverLockscreen;
@@ -166,7 +170,8 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte
NotificationPresenter presenter,
NotificationPanelViewController panel,
ActivityLaunchAnimator activityLaunchAnimator,
NotificationLaunchAnimatorControllerProvider notificationAnimationProvider) {
NotificationLaunchAnimatorControllerProvider notificationAnimationProvider,
LaunchEventsEmitter launchEventsEmitter) {
mContext = context;
mCommandQueue = commandQueue;
mMainThreadHandler = mainThreadHandler;
@@ -192,18 +197,17 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte
mLockPatternUtils = lockPatternUtils;
mStatusBarRemoteInputCallback = remoteInputCallback;
mActivityIntentHelper = activityIntentHelper;
mNotifPipelineFlags = notifPipelineFlags;
mMetricsLogger = metricsLogger;
mLogger = logger;
mOnUserInteractionCallback = onUserInteractionCallback;
// TODO: use KeyguardStateController#isOccluded to remove this dependency
mCentralSurfaces = centralSurfaces;
mPresenter = presenter;
mNotificationPanel = panel;
mActivityLaunchAnimator = activityLaunchAnimator;
mNotificationAnimationProvider = notificationAnimationProvider;
mLaunchEventsEmitter = launchEventsEmitter;
if (!mNotifPipelineFlags.isNewPipelineEnabled()) {
mEntryManager.addNotificationEntryListener(new NotificationEntryListener() {
@@ -254,6 +258,8 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte
return;
}
mLaunchEventsEmitter.notifyStartLaunchNotifActivity(entry);
boolean isActivityIntent = intent != null && intent.isActivity() && !isBubble;
final boolean willLaunchResolverActivity = isActivityIntent
&& mActivityIntentHelper.wouldLaunchResolverActivity(intent.getIntent(),
@@ -280,7 +286,9 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte
postKeyguardAction.onDismiss();
} else {
mActivityStarter.dismissKeyguardThenExecute(
postKeyguardAction, null /* cancel */, willLaunchResolverActivity);
postKeyguardAction,
() -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry),
willLaunchResolverActivity);
}
}
@@ -380,24 +388,26 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte
// inform NMS that the notification was clicked
mClickNotifier.onNotificationClick(notificationKey, nv);
if (!canBubble) {
if (shouldAutoCancel || mRemoteInputManager.isNotificationKeptForRemoteInputHistory(
notificationKey)) {
// Immediately remove notification from visually showing.
// We have to post the removal to the UI thread for synchronization.
mMainThreadHandler.post(() -> {
final Runnable removeNotification = () ->
mOnUserInteractionCallback.onDismiss(
entry, REASON_CLICK, summaryToRemove);
if (mPresenter.isCollapsing()) {
// To avoid lags we're only performing the remove
// after the shade is collapsed
mShadeController.addPostCollapseAction(removeNotification);
} else {
removeNotification.run();
}
});
}
if (!canBubble && (shouldAutoCancel
|| mRemoteInputManager.isNotificationKeptForRemoteInputHistory(notificationKey))) {
// Immediately remove notification from visually showing.
// We have to post the removal to the UI thread for synchronization.
mMainThreadHandler.post(() -> {
final Runnable removeNotification = () -> {
mOnUserInteractionCallback.onDismiss(entry, REASON_CLICK, summaryToRemove);
mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry);
};
if (mPresenter.isCollapsing()) {
// To avoid lags we're only performing the remove
// after the shade is collapsed
mShadeController.addPostCollapseAction(removeNotification);
} else {
removeNotification.run();
}
});
} else {
mMainThreadHandler.post(
() -> mLaunchEventsEmitter.notifyFinishLaunchNotifActivity(entry));
}
mIsCollapsingToShowActivityOverLockscreen = false;
@@ -659,4 +669,35 @@ class StatusBarNotificationActivityStarter implements NotificationActivityStarte
return entry.shouldSuppressFullScreenIntent();
}
@SysUISingleton
static class LaunchEventsEmitter implements NotifActivityLaunchEvents {
private final ListenerSet<Listener> mListeners = new ListenerSet<>();
@Inject
LaunchEventsEmitter() {}
@Override
public void registerListener(@NonNull Listener listener) {
mListeners.addIfAbsent(listener);
}
@Override
public void unregisterListener(@NonNull Listener listener) {
mListeners.remove(listener);
}
private void notifyStartLaunchNotifActivity(NotificationEntry entry) {
for (Listener listener : mListeners) {
listener.onStartLaunchNotifActivity(entry);
}
}
private void notifyFinishLaunchNotifActivity(NotificationEntry entry) {
for (Listener listener : mListeners) {
listener.onFinishLaunchNotifActivity(entry);
}
}
}
}

View File

@@ -27,7 +27,6 @@ import com.android.systemui.statusbar.NotificationShelfController;
import com.android.systemui.statusbar.core.StatusBarInitializer;
import com.android.systemui.statusbar.notification.NotificationActivityStarter;
import com.android.systemui.statusbar.notification.collection.inflation.NotificationRowBinderImpl;
import com.android.systemui.statusbar.notification.collection.render.StatusBarNotifPanelEventSourceModule;
import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController;
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutListContainerModule;
@@ -60,8 +59,8 @@ import dagger.Subcomponent;
* outside the component. Should more items be moved *into* this component to avoid so many getters?
*/
@Subcomponent(modules = {
CentralSurfacesStartableModule.class,
NotificationStackScrollLayoutListContainerModule.class,
StatusBarNotifPanelEventSourceModule.class,
StatusBarViewModule.class,
StatusBarNotificationActivityStarterModule.class,
StatusBarNotificationPresenterModule.class,

View File

@@ -0,0 +1,28 @@
/*
* Copyright (C) 2022 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.phone.dagger;
import java.util.Set;
import dagger.Module;
import dagger.multibindings.Multibinds;
@Module
interface CentralSurfacesStartableModule {
@Multibinds
Set<CentralSurfacesComponent.Startable> multibindStartables();
}

View File

@@ -0,0 +1,138 @@
/*
* Copyright (C) 2022 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.coordinator
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.statusbar.notification.collection.NotifPipeline
import com.android.systemui.statusbar.notification.collection.NotificationEntry
import com.android.systemui.statusbar.notification.collection.coordinator.dagger.CoordinatorScope
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender
import com.android.systemui.statusbar.phone.NotifActivityLaunchEvents
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
import dagger.BindsInstance
import dagger.Component
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when` as whenever
@SmallTest
class ActivityLaunchAnimCoordinatorTest : SysuiTestCase() {
val activityLaunchEvents: NotifActivityLaunchEvents = mock()
val pipeline: NotifPipeline = mock()
val coordinator: ActivityLaunchAnimCoordinator =
DaggerTestActivityStarterCoordinatorComponent
.factory()
.create(activityLaunchEvents)
.coordinator
@Test
fun testNoLifetimeExtensionIfNoAssociatedActivityLaunch() {
coordinator.attach(pipeline)
val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> {
verify(pipeline).addNotificationLifetimeExtender(capture())
}
val fakeEntry = mock<NotificationEntry>().also {
whenever(it.key).thenReturn("0")
}
assertFalse(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0))
}
@Test
fun testNoLifetimeExtensionIfAssociatedActivityLaunchAlreadyEnded() {
coordinator.attach(pipeline)
val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> {
verify(pipeline).addNotificationLifetimeExtender(capture())
}
val eventListener = withArgCaptor<NotifActivityLaunchEvents.Listener> {
verify(activityLaunchEvents).registerListener(capture())
}
val fakeEntry = mock<NotificationEntry>().also {
whenever(it.key).thenReturn("0")
}
eventListener.onStartLaunchNotifActivity(fakeEntry)
eventListener.onFinishLaunchNotifActivity(fakeEntry)
assertFalse(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0))
}
@Test
fun testLifetimeExtensionWhileActivityLaunchInProgress() {
coordinator.attach(pipeline)
val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> {
verify(pipeline).addNotificationLifetimeExtender(capture())
}
val eventListener = withArgCaptor<NotifActivityLaunchEvents.Listener> {
verify(activityLaunchEvents).registerListener(capture())
}
val onEndLifetimeExtensionCallback =
mock<NotifLifetimeExtender.OnEndLifetimeExtensionCallback>()
lifetimeExtender.setCallback(onEndLifetimeExtensionCallback)
val fakeEntry = mock<NotificationEntry>().also {
whenever(it.key).thenReturn("0")
}
eventListener.onStartLaunchNotifActivity(fakeEntry)
assertTrue(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0))
eventListener.onFinishLaunchNotifActivity(fakeEntry)
verify(onEndLifetimeExtensionCallback).onEndLifetimeExtension(lifetimeExtender, fakeEntry)
}
@Test
fun testCancelLifetimeExtensionDoesNotInvokeCallback() {
coordinator.attach(pipeline)
val lifetimeExtender = withArgCaptor<NotifLifetimeExtender> {
verify(pipeline).addNotificationLifetimeExtender(capture())
}
val eventListener = withArgCaptor<NotifActivityLaunchEvents.Listener> {
verify(activityLaunchEvents).registerListener(capture())
}
val onEndLifetimeExtensionCallback =
mock<NotifLifetimeExtender.OnEndLifetimeExtensionCallback>()
lifetimeExtender.setCallback(onEndLifetimeExtensionCallback)
val fakeEntry = mock<NotificationEntry>().also {
whenever(it.key).thenReturn("0")
}
eventListener.onStartLaunchNotifActivity(fakeEntry)
assertTrue(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0))
lifetimeExtender.cancelLifetimeExtension(fakeEntry)
eventListener.onFinishLaunchNotifActivity(fakeEntry)
verify(onEndLifetimeExtensionCallback, never())
.onEndLifetimeExtension(lifetimeExtender, fakeEntry)
}
}
@CoordinatorScope
@Component(modules = [ActivityLaunchAnimCoordinatorModule::class])
interface TestActivityStarterCoordinatorComponent {
val coordinator: ActivityLaunchAnimCoordinator
@Component.Factory
interface Factory {
fun create(
@BindsInstance activityLaunchEvents: NotifActivityLaunchEvents
): TestActivityStarterCoordinatorComponent
}
}

View File

@@ -41,7 +41,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifStabilityManager;
import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.Pluggable;
import com.android.systemui.statusbar.notification.collection.provider.VisualStabilityProvider;
import com.android.systemui.statusbar.notification.collection.render.NotifPanelEventSource;
import com.android.systemui.statusbar.phone.NotifPanelEvents;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.util.concurrency.FakeExecutor;
import com.android.systemui.util.time.FakeSystemClock;
@@ -67,12 +67,12 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase {
@Mock private StatusBarStateController mStatusBarStateController;
@Mock private Pluggable.PluggableListener<NotifStabilityManager> mInvalidateListener;
@Mock private HeadsUpManager mHeadsUpManager;
@Mock private NotifPanelEventSource mNotifPanelEventSource;
@Mock private NotifPanelEvents mNotifPanelEvents;
@Mock private VisualStabilityProvider mVisualStabilityProvider;
@Captor private ArgumentCaptor<WakefulnessLifecycle.Observer> mWakefulnessObserverCaptor;
@Captor private ArgumentCaptor<StatusBarStateController.StateListener> mSBStateListenerCaptor;
@Captor private ArgumentCaptor<NotifPanelEventSource.Callbacks> mNotifPanelEventsCallbackCaptor;
@Captor private ArgumentCaptor<NotifPanelEvents.Listener> mNotifPanelEventsCallbackCaptor;
@Captor private ArgumentCaptor<NotifStabilityManager> mNotifStabilityManagerCaptor;
private FakeSystemClock mFakeSystemClock = new FakeSystemClock();
@@ -80,7 +80,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase {
private WakefulnessLifecycle.Observer mWakefulnessObserver;
private StatusBarStateController.StateListener mStatusBarStateListener;
private NotifPanelEventSource.Callbacks mNotifPanelEventsCallback;
private NotifPanelEvents.Listener mNotifPanelEventsCallback;
private NotifStabilityManager mNotifStabilityManager;
private NotificationEntry mEntry;
@@ -92,7 +92,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase {
mFakeExecutor,
mDumpManager,
mHeadsUpManager,
mNotifPanelEventSource,
mNotifPanelEvents,
mStatusBarStateController,
mVisualStabilityProvider,
mWakefulnessLifecycle);
@@ -106,7 +106,7 @@ public class VisualStabilityCoordinatorTest extends SysuiTestCase {
verify(mStatusBarStateController).addCallback(mSBStateListenerCaptor.capture());
mStatusBarStateListener = mSBStateListenerCaptor.getValue();
verify(mNotifPanelEventSource).registerCallbacks(mNotifPanelEventsCallbackCaptor.capture());
verify(mNotifPanelEvents).registerListener(mNotifPanelEventsCallbackCaptor.capture());
mNotifPanelEventsCallback = mNotifPanelEventsCallbackCaptor.getValue();
verify(mNotifPipeline).setVisualStabilityManager(mNotifStabilityManagerCaptor.capture());

View File

@@ -361,6 +361,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
private SysUiState mSysUiState;
@Mock
private NotificationListContainer mNotificationListContainer;
private NotificationPanelViewController.PanelEventsEmitter mPanelEventsEmitter;
private Optional<SysUIUnfoldComponent> mSysUIUnfoldComponent = Optional.empty();
private SysuiStatusBarStateController mStatusBarStateController;
private NotificationPanelViewController mNotificationPanelViewController;
@@ -488,6 +489,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
}).when(mNotificationShadeWindowController).batchApplyWindowLayoutParams(any());
mMainHandler = new Handler(Looper.getMainLooper());
mPanelEventsEmitter = new NotificationPanelViewController.PanelEventsEmitter();
mNotificationPanelViewController = new NotificationPanelViewController(mView,
mResources,
@@ -546,7 +548,8 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase {
mQsFrameTranslateController,
mSysUiState,
mKeyguardUnlockAnimationController,
mNotificationListContainer);
mNotificationListContainer,
mPanelEventsEmitter);
mNotificationPanelViewController.initDependencies(
mCentralSurfaces,
() -> {},

View File

@@ -57,6 +57,7 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.animation.ActivityLaunchAnimator;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.NotificationClickNotifier;
@@ -130,7 +131,6 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
private NotifPipeline mNotifPipeline;
@Mock
private NotificationVisibilityProvider mVisibilityProvider;
@Mock
private ActivityIntentHelper mActivityIntentHelper;
@Mock
@@ -145,14 +145,14 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
private ActivityLaunchAnimator mActivityLaunchAnimator;
@Mock
private InteractionJankMonitor mJankMonitor;
private StatusBarNotificationActivityStarter.LaunchEventsEmitter mLaunchEventsEmitter;
private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock());
private NotificationTestHelper mNotificationTestHelper;
private ExpandableNotificationRow mNotificationRow;
private ExpandableNotificationRow mBubbleNotificationRow;
private final Answer<Void> mCallOnDismiss = answerVoid(
(ActivityStarter.OnDismissAction dismissAction, Runnable cancel,
(OnDismissAction dismissAction, Runnable cancel,
Boolean afterKeyguardGone) -> dismissAction.onDismiss());
private ArrayList<NotificationEntry> mActiveNotifications;
@@ -201,7 +201,7 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
NotificationListContainer.class),
headsUpManager,
mJankMonitor);
mLaunchEventsEmitter = new StatusBarNotificationActivityStarter.LaunchEventsEmitter();
mNotificationActivityStarter =
new StatusBarNotificationActivityStarter(
getContext(),
@@ -237,12 +237,13 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
mock(NotificationPresenter.class),
mock(NotificationPanelViewController.class),
mActivityLaunchAnimator,
notificationAnimationProvider
notificationAnimationProvider,
mLaunchEventsEmitter
);
// set up dismissKeyguardThenExecute to synchronously invoke the OnDismissAction arg
doAnswer(mCallOnDismiss).when(mActivityStarter).dismissKeyguardThenExecute(
any(ActivityStarter.OnDismissAction.class), any(), anyBoolean());
any(OnDismissAction.class), any(), anyBoolean());
// set up addAfterKeyguardGoneRunnable to synchronously invoke the Runnable arg
doAnswer(answerVoid(Runnable::run))
@@ -402,4 +403,40 @@ public class StatusBarNotificationActivityStarterTest extends SysuiTestCase {
// THEN display should try wake up for the full screen intent
verify(mCentralSurfaces).wakeUpForFullScreenIntent();
}
@Test
public void testNotifActivityStarterEventSourceStartEvent_onNotificationClicked() {
NotifActivityLaunchEvents.Listener listener =
mock(NotifActivityLaunchEvents.Listener.class);
mLaunchEventsEmitter.registerListener(listener);
mNotificationActivityStarter
.onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow);
verify(listener).onStartLaunchNotifActivity(mNotificationRow.getEntry());
}
@Test
public void testNotifActivityStarterEventSourceFinishEvent_dismissKeyguardCancelled() {
NotifActivityLaunchEvents.Listener listener =
mock(NotifActivityLaunchEvents.Listener.class);
mLaunchEventsEmitter.registerListener(listener);
// set up dismissKeyguardThenExecute to synchronously invoke the cancel runnable arg
doAnswer(answerVoid(
(OnDismissAction dismissAction, Runnable cancel, Boolean afterKeyguardGone) ->
cancel.run()))
.when(mActivityStarter)
.dismissKeyguardThenExecute(any(OnDismissAction.class), any(), anyBoolean());
mNotificationActivityStarter
.onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow);
verify(listener).onFinishLaunchNotifActivity(mNotificationRow.getEntry());
}
@Test
public void testNotifActivityStarterEventSourceFinishEvent_postPanelCollapse() {
NotifActivityLaunchEvents.Listener listener =
mock(NotifActivityLaunchEvents.Listener.class);
mLaunchEventsEmitter.registerListener(listener);
mNotificationActivityStarter
.onNotificationClicked(mNotificationRow.getEntry().getSbn(), mNotificationRow);
verify(listener).onFinishLaunchNotifActivity(mNotificationRow.getEntry());
}
}