From 21647dd91ce5c231e38b82d77b92208e8dd05abe Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Fri, 25 Feb 2022 16:39:10 -0500 Subject: [PATCH] Extend lifetime of notifs during activity launch Clicking on a notification can result in a chain of asynchronous code, depending on the state of the shade and keyguard. If the notification that was clicked is cancelled during this async flow, then when it's finally complete, if the notification has FLAG_AUTO_CANCEL, SystemUI will attempt to dismiss the notification, which is no longer there according to the pipeline, due to the aforementioned cancellation. This CL fixes the issue by introducing a new ActivityLaunchAnimCoordinater that registers a lifetime extender to keep notifications alive during their launch animation. Fixes: 219939676 Test: atest SystemUITests Test: 1. Install Notify.apk (go/notify-apk) 2. In the Notify app: Check off Delayed 5000 and Auto Cancel 3. Click "Add" and spam click "Update" (5-10 times should suffice) 4. Expand notification shade 5. Immediately tap the Notify notification when it gets posted Observe: no crashing Change-Id: I819ad840845df1bfa3acd71187c6ba47de9a64b7 --- .../ActivityLaunchAnimCoordinator.kt | 95 ++++++++++++ .../coordinator/NotifCoordinators.kt | 6 +- .../SensitiveContentCoordinator.kt | 6 +- .../VisualStabilityCoordinator.java | 12 +- .../coordinator/dagger/CoordinatorsModule.kt | 8 +- .../render/NotifPanelEventSource.kt | 128 ---------------- .../dagger/NotificationsModule.java | 10 +- .../phone/NotifActivityLaunchEvents.kt | 39 +++++ .../NotifActivityLaunchEventsModule.java | 30 ++++ .../statusbar/phone/NotifPanelEvents.kt | 39 +++++ .../phone/NotifPanelEventsModule.java | 30 ++++ .../NotificationPanelViewController.java | 61 +++++--- .../StatusBarNotificationActivityStarter.java | 85 ++++++++--- .../dagger/CentralSurfacesComponent.java | 3 +- .../CentralSurfacesStartableModule.java | 28 ++++ .../ActivityLaunchAnimCoordinatorTest.kt | 138 ++++++++++++++++++ .../VisualStabilityCoordinatorTest.java | 12 +- .../NotificationPanelViewControllerTest.java | 5 +- ...tusBarNotificationActivityStarterTest.java | 49 ++++++- 19 files changed, 580 insertions(+), 204 deletions(-) create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt delete mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/NotifPanelEventSource.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEventsModule.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifPanelEvents.kt create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifPanelEventsModule.java create mode 100644 packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt new file mode 100644 index 0000000000000..b54163d29e800 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinator.kt @@ -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() + + 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 + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt index 757fb5a2fe9a3..be20df460bb05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/NotifCoordinators.kt @@ -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 = 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" } -} \ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt index 72d091883ce60..3f8a39f62dfb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/SensitiveContentCoordinator.kt @@ -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() - - 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 - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java index 51bbf1c804781..efe88e6d4f0a0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/NotificationsModule.java @@ -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 { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt new file mode 100644 index 0000000000000..f46d073382069 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEvents.kt @@ -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) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEventsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEventsModule.java new file mode 100644 index 0000000000000..84ff538677b0d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifActivityLaunchEventsModule.java @@ -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); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifPanelEvents.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifPanelEvents.kt new file mode 100644 index 0000000000000..a385e22c1affc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifPanelEvents.kt @@ -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) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifPanelEventsModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifPanelEventsModule.java new file mode 100644 index 0000000000000..2aaf6a5f43910 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotifPanelEventsModule.java @@ -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); +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java index 61760fb8c78c4..292932f931824 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/NotificationPanelViewController.java @@ -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 mNotificationPanelUnfoldAnimationController; - private final ListenerSet 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(); @@ -3460,9 +3460,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); } } @@ -3471,9 +3469,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); } } @@ -4375,16 +4371,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) { @@ -5158,4 +5144,35 @@ public class NotificationPanelViewController extends PanelViewController 1.0f /* speedUpFactor */); } } + + @SysUISingleton + static class PanelEventsEmitter implements NotifPanelEvents { + + private final ListenerSet 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); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index edbddbb3c3e79..108d98a129b2d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -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 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); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java index 59c9d0baea3fb..a86ad6bc70125 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesComponent.java @@ -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, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java new file mode 100644 index 0000000000000..21e5ad5778b12 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/CentralSurfacesStartableModule.java @@ -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 multibindStartables(); +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt new file mode 100644 index 0000000000000..c6c043aafb208 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ActivityLaunchAnimCoordinatorTest.kt @@ -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 { + verify(pipeline).addNotificationLifetimeExtender(capture()) + } + val fakeEntry = mock().also { + whenever(it.key).thenReturn("0") + } + assertFalse(lifetimeExtender.maybeExtendLifetime(fakeEntry, 0)) + } + + @Test + fun testNoLifetimeExtensionIfAssociatedActivityLaunchAlreadyEnded() { + coordinator.attach(pipeline) + val lifetimeExtender = withArgCaptor { + verify(pipeline).addNotificationLifetimeExtender(capture()) + } + val eventListener = withArgCaptor { + verify(activityLaunchEvents).registerListener(capture()) + } + val fakeEntry = mock().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 { + verify(pipeline).addNotificationLifetimeExtender(capture()) + } + val eventListener = withArgCaptor { + verify(activityLaunchEvents).registerListener(capture()) + } + val onEndLifetimeExtensionCallback = + mock() + lifetimeExtender.setCallback(onEndLifetimeExtensionCallback) + + val fakeEntry = mock().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 { + verify(pipeline).addNotificationLifetimeExtender(capture()) + } + val eventListener = withArgCaptor { + verify(activityLaunchEvents).registerListener(capture()) + } + val onEndLifetimeExtensionCallback = + mock() + lifetimeExtender.setCallback(onEndLifetimeExtensionCallback) + + val fakeEntry = mock().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 + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java index ee111715e5ea9..6f8e5d8e514e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorTest.java @@ -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 mInvalidateListener; @Mock private HeadsUpManager mHeadsUpManager; - @Mock private NotifPanelEventSource mNotifPanelEventSource; + @Mock private NotifPanelEvents mNotifPanelEvents; @Mock private VisualStabilityProvider mVisualStabilityProvider; @Captor private ArgumentCaptor mWakefulnessObserverCaptor; @Captor private ArgumentCaptor mSBStateListenerCaptor; - @Captor private ArgumentCaptor mNotifPanelEventsCallbackCaptor; + @Captor private ArgumentCaptor mNotifPanelEventsCallbackCaptor; @Captor private ArgumentCaptor 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()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java index 38412fd1fa9bc..e58f557844b8a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/NotificationPanelViewControllerTest.java @@ -361,6 +361,7 @@ public class NotificationPanelViewControllerTest extends SysuiTestCase { private SysUiState mSysUiState; @Mock private NotificationListContainer mNotificationListContainer; + private NotificationPanelViewController.PanelEventsEmitter mPanelEventsEmitter; private Optional 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, () -> {}, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java index ace7415f2c171..27179fd7be92b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterTest.java @@ -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 mCallOnDismiss = answerVoid( - (ActivityStarter.OnDismissAction dismissAction, Runnable cancel, + (OnDismissAction dismissAction, Runnable cancel, Boolean afterKeyguardGone) -> dismissAction.onDismiss()); private ArrayList 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()); + } }