Merge "Force FGS notifications to show for a minimum time" into oc-dev
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtender.java b/packages/SystemUI/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtender.java
new file mode 100644
index 0000000..1c1f388
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtender.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.ArraySet;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.systemui.statusbar.NotificationData;
+
+/**
+ * Extends the lifetime of foreground notification services such that they show for at least
+ * five seconds
+ */
+public class ForegroundServiceLifetimeExtender implements NotificationLifetimeExtender {
+    private static final String TAG = "FGSLifetimeExtender";
+
+    @VisibleForTesting
+    public static final int MIN_FGS_TIME_MS = 5000;
+
+    private NotificationSafeToRemoveCallback mNotificationSafeToRemoveCallback;
+    private ArraySet<NotificationData.Entry> mManagedEntries = new ArraySet<>();
+    private Handler mHandler = new Handler(Looper.getMainLooper());
+
+    public ForegroundServiceLifetimeExtender() {
+    }
+
+    @Override
+    public void setCallback(@NonNull NotificationSafeToRemoveCallback callback) {
+        mNotificationSafeToRemoveCallback = callback;
+    }
+
+    @Override
+    public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
+        if ((entry.notification.getNotification().flags
+                & Notification.FLAG_FOREGROUND_SERVICE) == 0) {
+            return false;
+        }
+        long currentTime = System.currentTimeMillis();
+        return currentTime - entry.notification.getPostTime() < MIN_FGS_TIME_MS;
+    }
+
+    @Override
+    public boolean shouldExtendLifetimeForPendingNotification(
+            @NonNull NotificationData.Entry entry) {
+        return shouldExtendLifetime(entry);
+    }
+
+    @Override
+    public void setShouldManageLifetime(
+            @NonNull NotificationData.Entry entry, boolean shouldManage) {
+        android.util.Log.d("FGSExtender", "setShouldManageLifetime " + shouldManage);
+        if (!shouldManage) {
+            mManagedEntries.remove(entry);
+            return;
+        }
+        mManagedEntries.add(entry);
+        Runnable r = () -> {
+            if (mManagedEntries.contains(entry)) {
+                mManagedEntries.remove(entry);
+                if (mNotificationSafeToRemoveCallback != null) {
+                    mNotificationSafeToRemoveCallback.onSafeToRemove(entry.key);
+                }
+            }
+        };
+        long delayAmt = MIN_FGS_TIME_MS
+                - (System.currentTimeMillis() - entry.notification.getPostTime());
+        mHandler.postDelayed(r, delayAmt);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java
new file mode 100644
index 0000000..19edfaa
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import android.annotation.NonNull;
+
+import com.android.systemui.statusbar.NotificationData;
+
+/**
+ * Interface for anything that may need to keep notifications managed even after
+ * {@link NotificationListener} removes it.  The lifetime extender is in charge of performing the
+ * callback when the notification is then safe to remove.
+ */
+public interface NotificationLifetimeExtender {
+
+    /**
+     * Set the handler to callback to when the notification is safe to remove.
+     *
+     * @param callback the handler to callback
+     */
+    void setCallback(@NonNull NotificationSafeToRemoveCallback callback);
+
+    /**
+     * Determines whether or not the extender needs the notification kept after removal.
+     *
+     * @param entry the entry containing the notification to check
+     * @return true if the notification lifetime should be extended
+     */
+    boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry);
+
+    /**
+     * It's possible that a notification was canceled before it ever became visible. This callback
+     * gives lifetime extenders a chance to make sure it shows up. For example if a foreground
+     * service is canceled too quickly but we still want to make sure a FGS notification shows.
+     * @param pendingEntry the canceled (but pending) entry
+     * @return true if the notification lifetime should be extended
+     */
+    default boolean shouldExtendLifetimeForPendingNotification(
+            @NonNull NotificationData.Entry pendingEntry) {
+        return false;
+    }
+
+    /**
+     * Sets whether or not the lifetime should be managed by the extender.  In practice, if
+     * shouldManage is true, this is where the extender starts managing the entry internally and is
+     * now responsible for calling {@link NotificationSafeToRemoveCallback#onSafeToRemove(String)}
+     * when the entry is safe to remove.  If shouldManage is false, the extender no longer needs to
+     * worry about it (either because we will be removing it anyway or the entry is no longer
+     * removed due to an update).
+     *
+     * @param entry the entry that needs an extended lifetime
+     * @param shouldManage true if the extender should manage the entry now, false otherwise
+     */
+    void setShouldManageLifetime(@NonNull NotificationData.Entry entry, boolean shouldManage);
+
+    /**
+     * The callback for when the notification is now safe to remove (i.e. its lifetime has ended).
+     */
+    interface NotificationSafeToRemoveCallback {
+        /**
+         * Called when the lifetime extender determines it's safe to remove.
+         *
+         * @param key key of the entry that is now safe to remove
+         */
+        void onSafeToRemove(String key);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
index d2994bd..74d96b1 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBar.java
@@ -104,6 +104,7 @@
 import android.service.vr.IVrStateCallbacks;
 import android.text.TextUtils;
 import android.util.ArraySet;
+import android.util.ArrayMap;
 import android.util.DisplayMetrics;
 import android.util.EventLog;
 import android.util.Log;
@@ -134,6 +135,7 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
@@ -191,6 +193,7 @@
 import com.android.systemui.statusbar.DragDownHelper;
 import com.android.systemui.statusbar.EmptyShadeView;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.ForegroundServiceLifetimeExtender;
 import com.android.systemui.statusbar.GestureRecorder;
 import com.android.systemui.statusbar.KeyboardShortcuts;
 import com.android.systemui.statusbar.KeyguardIndicationController;
@@ -198,6 +201,7 @@
 import com.android.systemui.statusbar.NotificationData.Entry;
 import com.android.systemui.statusbar.NotificationGuts;
 import com.android.systemui.statusbar.NotificationInfo;
+import com.android.systemui.statusbar.NotificationLifetimeExtender;
 import com.android.systemui.statusbar.NotificationShelf;
 import com.android.systemui.statusbar.NotificationSnooze;
 import com.android.systemui.statusbar.RemoteInputController;
@@ -715,7 +719,7 @@
     private ConfigurationListener mConfigurationListener;
     private boolean mReinflateNotificationsOnUserSwitched;
     private HashMap<String, Entry> mPendingNotifications = new HashMap<>();
-    private ForegroundServiceController mForegroundServiceController;
+    protected ForegroundServiceController mForegroundServiceController;
 
     private void recycleAllVisibilityObjects(ArraySet<NotificationVisibility> array) {
         final int N = array.size();
@@ -760,6 +764,9 @@
         mSystemServicesProxy = SystemServicesProxy.getInstance(mContext);
 
         mForegroundServiceController = Dependency.get(ForegroundServiceController.class);
+        mFGSExtender = new ForegroundServiceLifetimeExtender();
+        mFGSExtender.setCallback(key -> removeNotification(key, mLatestRankingMap));
+
 
         mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
         mDisplay = mWindowManager.getDefaultDisplay();
@@ -1733,6 +1740,11 @@
         }
         Entry entry = mNotificationData.get(key);
 
+        if (entry != null && mFGSExtender.shouldExtendLifetime(entry)) {
+            extendLifetime(entry, mFGSExtender);
+            return;
+        }
+
         if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
                 && (entry.row != null && !entry.row.isDismissed())) {
             mLatestRankingMap = ranking;
@@ -1763,9 +1775,35 @@
                 }
             }
         }
+        // Make sure no lifetime extension is happening anymore
+        cancelLifetimeExtension(entry);
         setAreThereNotifications();
     }
 
+    /** Lifetime extension keeps entries around after they would've otherwise been canceled */
+    private void extendLifetime(Entry entry, NotificationLifetimeExtender extender) {
+        // Cancel any other extender which might be holding on to this notification entry
+        NotificationLifetimeExtender activeExtender = mRetainedNotifications.get(entry);
+        if (activeExtender != null && activeExtender != extender) {
+            activeExtender.setShouldManageLifetime(entry, false);
+        }
+        mRetainedNotifications.put(entry, extender);
+        extender.setShouldManageLifetime(entry, true);
+    }
+
+    /** Tells the current extender (if any) to stop extending the entry's lifetime */
+    private void cancelLifetimeExtension(NotificationData.Entry entry) {
+        NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry);
+        if (activeExtender != null) {
+            activeExtender.setShouldManageLifetime(entry, false);
+        }
+    }
+
+    @VisibleForTesting
+    public Map<Entry, NotificationLifetimeExtender> getRetainedNotificationMap() {
+        return mRetainedNotifications;
+    }
+
     /**
      * Ensures that the group children are cancelled immediately when the group summary is cancelled
      * instead of waiting for the notification manager to send all cancels. Otherwise this could
@@ -3369,6 +3407,17 @@
             }
         }
 
+        pw.println("  Lifetime-extended notifications:");
+        if (mRetainedNotifications.isEmpty()) {
+            pw.println("    None");
+        } else {
+            for (Map.Entry<NotificationData.Entry, NotificationLifetimeExtender> entry
+                    : mRetainedNotifications.entrySet()) {
+                pw.println("    " + entry.getKey().notification + " retained by "
+                        + entry.getValue().getClass().getName());
+            }
+        }
+
         pw.print("  mInteractingWindows="); pw.println(mInteractingWindows);
         pw.print("  mStatusBarWindowState=");
         pw.println(windowStateToString(mStatusBarWindowState));
@@ -5212,6 +5261,11 @@
 
     protected RemoteInputController mRemoteInputController;
 
+    // A lifetime extender that watches for foreground service notifications
+    @VisibleForTesting protected NotificationLifetimeExtender mFGSExtender;
+    private final Map<Entry, NotificationLifetimeExtender> mRetainedNotifications =
+        new ArrayMap<>();
+
     // for heads up notifications
     protected HeadsUpManager mHeadsUpManager;
 
@@ -6825,6 +6879,9 @@
             mRemoteInputEntriesToRemoveOnCollapse.remove(entry);
         }
 
+        // No need to keep the lifetime extension around if an update comes in for it
+        cancelLifetimeExtension(entry);
+
         Notification n = notification.getNotification();
         mNotificationData.updateRanking(ranking);
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtenderTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtenderTest.java
new file mode 100644
index 0000000..153efde
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtenderTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar;
+
+import static com.android.systemui.statusbar.ForegroundServiceLifetimeExtender.MIN_FGS_TIME_MS;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.Notification;
+import android.service.notification.NotificationListenerService.Ranking;
+import android.service.notification.StatusBarNotification;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.systemui.R;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.NotificationData.Entry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ForegroundServiceLifetimeExtenderTest extends SysuiTestCase {
+    private ForegroundServiceLifetimeExtender mExtender = new ForegroundServiceLifetimeExtender();
+    private StatusBarNotification mSbn;
+    private NotificationData.Entry mEntry;
+    private Notification mNotif;
+
+    @Before
+    public void setup() {
+        mNotif = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setContentTitle("Title")
+                .setContentText("Text")
+                .build();
+        mSbn = mock(StatusBarNotification.class);
+        when(mSbn.getNotification()).thenReturn(mNotif);
+        mEntry = new NotificationData.Entry(mSbn);
+    }
+
+    /**
+     * ForegroundServiceLifetimeExtenderTest
+     */
+    @Test
+    public void testShouldExtendLifetime_should_foreground() {
+        // Extend the lifetime of a FGS notification iff it has not been visible
+        // for the minimum time
+        mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE;
+        when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis());
+        assertTrue(mExtender.shouldExtendLifetime(mEntry));
+    }
+
+    @Test
+    public void testShouldExtendLifetime_shouldNot_foreground() {
+        mNotif.flags |= Notification.FLAG_FOREGROUND_SERVICE;
+        when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1);
+        assertFalse(mExtender.shouldExtendLifetime(mEntry));
+    }
+
+    @Test
+    public void testShouldExtendLifetime_shouldNot_notForeground() {
+        mNotif.flags = 0;
+        when(mSbn.getPostTime()).thenReturn(System.currentTimeMillis() - MIN_FGS_TIME_MS - 1);
+        assertFalse(mExtender.shouldExtendLifetime(mEntry));
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
index 0e3ea7a..d476123 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarTest.java
@@ -18,6 +18,8 @@
 
 import static android.app.NotificationManager.IMPORTANCE_HIGH;
 
+import static com.android.systemui.statusbar.ForegroundServiceLifetimeExtender.MIN_FGS_TIME_MS;
+
 import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertTrue;
 import static junit.framework.TestCase.fail;
@@ -37,6 +39,7 @@
 import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.verify;
 
+import android.app.ActivityManager;
 import android.app.Notification;
 import android.metrics.LogMaker;
 import android.os.Handler;
@@ -47,6 +50,7 @@
 import android.os.RemoteException;
 import android.os.UserHandle;
 import android.service.notification.StatusBarNotification;
+import android.service.notification.NotificationListenerService.RankingMap;
 import android.support.test.filters.SmallTest;
 import android.support.test.metricshelper.MetricsAsserts;
 import android.support.test.runner.AndroidJUnit4;
@@ -61,6 +65,11 @@
 import com.android.internal.logging.testing.FakeMetricsLogger;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.keyguard.KeyguardHostView.OnDismissAction;
+import com.android.systemui.ForegroundServiceController;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.ForegroundServiceLifetimeExtender;
+import com.android.systemui.statusbar.NotificationLifetimeExtender;
+import com.android.systemui.statusbar.RemoteInputController;
 import com.android.systemui.SysuiTestCase;
 import com.android.systemui.recents.misc.SystemServicesProxy;
 import com.android.systemui.statusbar.ActivatableNotificationView;
@@ -75,13 +84,19 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+import junit.framework.Assert;
+
 import java.util.ArrayList;
+import java.util.Map;
 
 @SmallTest
 @RunWith(AndroidTestingRunner.class)
 @RunWithLooper
 public class StatusBarTest extends SysuiTestCase {
 
+    private static final String TEST_PACKAGE_NAME = "test";
+    private static final int TEST_UID = 123;
+
     StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
     UnlockMethodCache mUnlockMethodCache;
     KeyguardIndicationController mKeyguardIndicationController;
@@ -94,8 +109,12 @@
     SystemServicesProxy mSystemServicesProxy;
     NotificationPanelView mNotificationPanelView;
     IStatusBarService mBarService;
+    RemoteInputController mRemoteInputController;
+    ForegroundServiceController mForegroundServiceController;
     ArrayList<Entry> mNotificationList;
     private DisplayMetrics mDisplayMetrics = new DisplayMetrics();
+    private ForegroundServiceLifetimeExtender mFGSExtender =
+        new ForegroundServiceLifetimeExtender();
 
     @Before
     public void setup() throws Exception {
@@ -110,6 +129,8 @@
         mNotificationPanelView = mock(NotificationPanelView.class);
         mNotificationList = mock(ArrayList.class);
         IPowerManager powerManagerService = mock(IPowerManager.class);
+        mRemoteInputController = mock(RemoteInputController.class);
+        mForegroundServiceController = mock(ForegroundServiceController.class);
         HandlerThread handlerThread = new HandlerThread("TestThread");
         handlerThread.start();
         mPowerManager = new PowerManager(mContext, powerManagerService,
@@ -118,10 +139,11 @@
         mBarService = mock(IStatusBarService.class);
 
         mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
+        mFGSExtender.setCallback(key -> mStatusBar.removeNotification(key, mock(RankingMap.class)));
         mStatusBar = new TestableStatusBar(mStatusBarKeyguardViewManager, mUnlockMethodCache,
                 mKeyguardIndicationController, mStackScroller, mHeadsUpManager,
                 mNotificationData, mPowerManager, mSystemServicesProxy, mNotificationPanelView,
-                mBarService);
+                mBarService, mFGSExtender, mRemoteInputController, mForegroundServiceController);
         doAnswer(invocation -> {
             OnDismissAction onDismissAction = (OnDismissAction) invocation.getArguments()[0];
             onDismissAction.onDismiss();
@@ -318,6 +340,53 @@
     }
 
     @Test
+    public void testForegroundServiceNotificationKeptForFiveSeconds() throws Exception {
+        RankingMap rm = mock(RankingMap.class);
+
+        // sbn posted "just now"
+        Notification n = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setContentTitle("Title")
+                .setContentText("Text")
+                .build();
+        n.flags |= Notification.FLAG_FOREGROUND_SERVICE;
+        StatusBarNotification sbn =
+                new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
+                0, n, new UserHandle(ActivityManager.getCurrentUser()), null,
+                System.currentTimeMillis());
+        NotificationData.Entry entry = new NotificationData.Entry(sbn);
+        when(mNotificationData.get(any())).thenReturn(entry);
+        mStatusBar.removeNotification(sbn.getKey(), rm);
+        Map<NotificationData.Entry, NotificationLifetimeExtender> map =
+                mStatusBar.getRetainedNotificationMap();
+        Assert.assertTrue(map.containsKey(entry));
+    }
+    @Test
+    public void testForegroundServiceNotification_notRetainedIfShownForFiveSeconds() 
+        throws Exception {
+
+        RankingMap rm = mock(RankingMap.class);
+
+        // sbn posted "more than 5 seconds ago"
+        Notification n = new Notification.Builder(mContext, "")
+                .setSmallIcon(R.drawable.ic_person)
+                .setContentTitle("Title")
+                .setContentText("Text")
+                .build();
+        n.flags |= Notification.FLAG_FOREGROUND_SERVICE;
+        StatusBarNotification sbn =
+            new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
+                0, n, new UserHandle(ActivityManager.getCurrentUser()), null,
+                System.currentTimeMillis() - MIN_FGS_TIME_MS - 1);
+        NotificationData.Entry entry = new NotificationData.Entry(sbn);
+        when(mNotificationData.get(any())).thenReturn(entry);
+        mStatusBar.removeNotification(sbn.getKey(), rm);
+        Map<NotificationData.Entry, NotificationLifetimeExtender> map =
+                mStatusBar.getRetainedNotificationMap();
+        Assert.assertFalse(map.containsKey(entry));
+    }
+
+    @Test
     public void testLogHidden() {
         try {
             mStatusBar.handleVisibleToUserChanged(false);
@@ -390,7 +459,8 @@
                 UnlockMethodCache unlock, KeyguardIndicationController key,
                 NotificationStackScrollLayout stack, HeadsUpManager hum, NotificationData nd,
                 PowerManager pm, SystemServicesProxy ssp, NotificationPanelView panelView,
-                IStatusBarService barService) {
+                IStatusBarService barService, ForegroundServiceLifetimeExtender fgsExtender,
+                RemoteInputController ric, ForegroundServiceController fsc) {
             mStatusBarKeyguardViewManager = man;
             mUnlockMethodCache = unlock;
             mKeyguardIndicationController = key;
@@ -402,10 +472,13 @@
             mSystemServicesProxy = ssp;
             mNotificationPanel = panelView;
             mBarService = barService;
+            mFGSExtender = fgsExtender;
+            mRemoteInputController = ric;
+            mForegroundServiceController = fsc;
         }
 
         public void setBarStateForTest(int state) {
             mState = state;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 8a7d600..1a73038 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -4385,8 +4385,15 @@
                         userId, mustHaveFlags, mustNotHaveFlags, reason, listenerName);
 
                 synchronized (mNotificationLock) {
-                    // Look for the notification, searching both the posted and enqueued lists.
-                    NotificationRecord r = findNotificationLocked(pkg, tag, id, userId);
+                    // If the notification is currently enqueued, repost this runnable so it has a
+                    // chance to notify listeners
+                    if ((findNotificationByListLocked(
+                            mEnqueuedNotifications, pkg, tag, id, userId)) != null) {
+                        mHandler.post(this);
+                    }
+                    // Look for the notification in the posted list, since we already checked enq
+                    NotificationRecord r = findNotificationByListLocked(
+                            mNotificationList, pkg, tag, id, userId);
                     if (r != null) {
                         // The notification was found, check if it should be removed.
 
diff --git a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
index d7815a2..d25721e 100644
--- a/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/notification/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -319,6 +319,21 @@
         assertEquals(0, mNotificationManagerService.getNotificationRecordCount());
     }
 
+
+    @Test
+    public void testCancelImmediatelyAfterEnqueueNotifiesListeners_ForegroundServiceFlag()
+            throws Exception {
+        final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
+        sbn.getNotification().flags =
+                Notification.FLAG_ONGOING_EVENT | Notification.FLAG_FOREGROUND_SERVICE;
+        mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag",
+                sbn.getId(), sbn.getNotification(), sbn.getUserId());
+        mBinderService.cancelNotificationWithTag(PKG, "tag", sbn.getId(), sbn.getUserId());
+        waitForIdle();
+        verify(mNotificationListeners, times(1)).notifyPostedLocked(any(), any());
+        verify(mNotificationListeners, times(1)).notifyRemovedLocked(any(), anyInt());
+    }
+
     @Test
     public void testCancelNotificationWhilePostedAndEnqueued() throws Exception {
         mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag", 0,