Merge "Allow the visual dismissal of foreground service notifs"
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
index b232efc..3322834 100644
--- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
+++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java
@@ -253,6 +253,13 @@
     public static final String NOTIFICATIONS_USE_PEOPLE_FILTERING =
             "notifications_use_people_filtering";
 
+    /**
+     * (boolean) Whether or not to enable user dismissing of foreground service notifications
+     * into a new section at the bottom of the notification shade.
+     */
+    public static final String NOTIFICATIONS_ALLOW_FGS_DISMISSAL =
+            "notifications_allow_fgs_dismissal";
+
     // Flags related to brightline falsing
 
     /**
diff --git a/packages/SystemUI/res/drawable/notif_dungeon_bg_gradient.xml b/packages/SystemUI/res/drawable/notif_dungeon_bg_gradient.xml
new file mode 100644
index 0000000..e456e29
--- /dev/null
+++ b/packages/SystemUI/res/drawable/notif_dungeon_bg_gradient.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<shape
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <gradient
+        android:angle="90"
+        android:startColor="#ff000000"
+        android:endColor="#00000000"
+        android:type="linear" />
+</shape>
diff --git a/packages/SystemUI/res/layout/foreground_service_dungeon.xml b/packages/SystemUI/res/layout/foreground_service_dungeon.xml
new file mode 100644
index 0000000..d4e98e2
--- /dev/null
+++ b/packages/SystemUI/res/layout/foreground_service_dungeon.xml
@@ -0,0 +1,61 @@
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.systemui.statusbar.notification.row.ForegroundServiceDungeonView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/foreground_service_dungeon"
+    android:layout_width="@dimen/qs_panel_width"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal|bottom"
+    android:visibility="visible"
+>
+    <LinearLayout
+        android:layout_height="wrap_content"
+        android:layout_width="match_parent"
+        android:orientation="vertical"
+        android:gravity="bottom"
+        android:visibility="visible"
+        android:background="@drawable/notif_dungeon_bg_gradient"
+    >
+
+        <!-- divider view -->
+        <View
+            android:layout_width="match_parent"
+            android:layout_height="1dp"
+            android:background="@color/GM2_grey_200"
+            android:visibility="visible"
+        />
+
+        <TextView
+            android:id="@+id/dungeon_title"
+            android:layout_height="48dp"
+            android:layout_width="match_parent"
+            android:padding="8dp"
+            android:text="Apps active in background"
+            android:textColor="@color/GM2_grey_200"
+        />
+
+        <!--  List containing the actual foreground service notifications  -->
+        <LinearLayout
+            android:id="@+id/entry_list"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:gravity="bottom"
+            android:orientation="vertical" >
+        </LinearLayout>
+
+    </LinearLayout>
+</com.android.systemui.statusbar.notification.row.ForegroundServiceDungeonView>
diff --git a/packages/SystemUI/res/layout/foreground_service_dungeon_row.xml b/packages/SystemUI/res/layout/foreground_service_dungeon_row.xml
new file mode 100644
index 0000000..a6f1638
--- /dev/null
+++ b/packages/SystemUI/res/layout/foreground_service_dungeon_row.xml
@@ -0,0 +1,43 @@
+<!--
+  ~ Copyright (C) 2020 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<com.android.systemui.statusbar.notification.row.DungeonRow
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/foreground_service_dungeon_row"
+    android:layout_width="match_parent"
+    android:layout_height="48dp"
+    android:padding="8dp"
+    android:clickable="true"
+    android:orientation="horizontal" >
+
+    <com.android.systemui.statusbar.StatusBarIconView
+        android:id="@+id/icon"
+        android:layout_width="24dp"
+        android:layout_height="24dp"
+        android:padding="4dp" />
+
+    <TextView
+        android:id="@+id/app_name"
+        android:layout_width="0dp"
+        android:layout_weight="1"
+        android:layout_height="wrap_content"
+        android:paddingStart="4dp"
+        android:gravity="center_vertical"
+        android:layout_gravity="center_vertical"
+        android:textColor="@color/GM2_grey_200"
+    />
+
+</com.android.systemui.statusbar.notification.row.DungeonRow>
diff --git a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
index a26cce0..7c07c9d 100644
--- a/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
+++ b/packages/SystemUI/src/com/android/systemui/bubbles/BubbleController.java
@@ -429,12 +429,11 @@
                     }
                 });
 
-        mNotificationEntryManager.setNotificationRemoveInterceptor(
+        mNotificationEntryManager.addNotificationRemoveInterceptor(
                 new NotificationRemoveInterceptor() {
                     @Override
-                    public boolean onNotificationRemoveRequested(String key, int reason) {
-                        NotificationEntry entry =
-                                mNotificationEntryManager.getActiveNotificationUnfiltered(key);
+                    public boolean onNotificationRemoveRequested(
+                            String key, NotificationEntry entry, int reason) {
                         return shouldInterceptDismissal(entry, reason);
                     }
                 });
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoveInterceptor.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoveInterceptor.java
index 930116e..caa1e2d 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoveInterceptor.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoveInterceptor.java
@@ -16,8 +16,12 @@
 
 package com.android.systemui.statusbar;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.service.notification.NotificationListenerService;
 
+import com.android.systemui.statusbar.notification.collection.NotificationEntry;
+
 /**
  * Interface for anything that may need to prevent notifications from being removed. This is
  * similar to a {@link NotificationLifetimeExtender} in the sense that it extends the life of
@@ -30,11 +34,15 @@
     /**
      * Called when a notification has been removed.
      *
-     * @param key the entry key of the notification being removed.
+     * @param key the key of the notification being removed. Never null
+     * @param entry the entry of the notification being removed.
      * @param removeReason why the notification is being removed, e.g.
      * {@link NotificationListenerService#REASON_CANCEL} or 0 if unknown.
      *
      * @return true if the removal should be ignored, false otherwise.
      */
-    boolean onNotificationRemoveRequested(String key, int removeReason);
+    boolean onNotificationRemoveRequested(
+            @NonNull String key,
+            @Nullable NotificationEntry entry,
+            int removeReason);
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
index 8d4a9ef..37f9f88 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationViewHierarchyManager.java
@@ -34,6 +34,7 @@
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 import com.android.systemui.statusbar.notification.collection.NotificationEntry;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
+import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectionController;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
@@ -81,6 +82,7 @@
     private final BubbleController mBubbleController;
     private final DynamicPrivacyController mDynamicPrivacyController;
     private final KeyguardBypassController mBypassController;
+    private final ForegroundServiceSectionController mFgsSectionController;
     private final Context mContext;
 
     private NotificationPresenter mPresenter;
@@ -101,7 +103,9 @@
             NotificationEntryManager notificationEntryManager,
             KeyguardBypassController bypassController,
             BubbleController bubbleController,
-            DynamicPrivacyController privacyController) {
+            DynamicPrivacyController privacyController,
+            ForegroundServiceSectionController fgsSectionController
+    ) {
         mContext = context;
         mHandler = mainHandler;
         mLockscreenUserManager = notificationLockscreenUserManager;
@@ -110,6 +114,7 @@
         mVisualStabilityManager = visualStabilityManager;
         mStatusBarStateController = (SysuiStatusBarStateController) statusBarStateController;
         mEntryManager = notificationEntryManager;
+        mFgsSectionController = fgsSectionController;
         Resources res = context.getResources();
         mAlwaysExpandNonGroupedNotification =
                 res.getBoolean(R.bool.config_alwaysExpandNonGroupedNotifications);
@@ -140,7 +145,8 @@
             boolean hideMedia = Utils.useQsMediaPlayer(mContext);
             if (ent.isRowDismissed() || ent.isRowRemoved()
                     || (ent.isMediaNotification() && hideMedia)
-                    || mBubbleController.isBubbleNotificationSuppressedFromShade(ent)) {
+                    || mBubbleController.isBubbleNotificationSuppressedFromShade(ent)
+                    || mFgsSectionController.hasEntry(ent)) {
                 // we don't want to update removed notifications because they could
                 // temporarily become children if they were isolated before.
                 continue;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ForegroundServiceDismissalFeatureController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ForegroundServiceDismissalFeatureController.kt
new file mode 100644
index 0000000..b1d6b40
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ForegroundServiceDismissalFeatureController.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification
+
+import android.content.Context
+import android.provider.DeviceConfig
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags.NOTIFICATIONS_ALLOW_FGS_DISMISSAL
+import com.android.systemui.util.DeviceConfigProxy
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private var sIsEnabled: Boolean? = null
+
+/**
+ * Feature controller for NOTIFICATIONS_ALLOW_FGS_DISMISSAL config.
+ */
+// TODO: this is really boilerplatey, make a base class that just wraps the device config
+@Singleton
+class ForegroundServiceDismissalFeatureController @Inject constructor(
+    val proxy: DeviceConfigProxy,
+    val context: Context
+) {
+    fun isForegroundServiceDismissalEnabled(): Boolean {
+        return isEnabled(proxy)
+    }
+}
+
+private fun isEnabled(proxy: DeviceConfigProxy): Boolean {
+    if (sIsEnabled == null) {
+        sIsEnabled = proxy.getBoolean(
+                DeviceConfig.NAMESPACE_SYSTEMUI, NOTIFICATIONS_ALLOW_FGS_DISMISSAL, false)
+    }
+
+    return sIsEnabled!!
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
index 4a22831..6bb377e8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationEntryManager.java
@@ -135,6 +135,7 @@
     private final NotificationGroupManager mGroupManager;
     private final NotificationRankingManager mRankingManager;
     private final FeatureFlags mFeatureFlags;
+    private final ForegroundServiceDismissalFeatureController mFgsFeatureController;
 
     private NotificationPresenter mPresenter;
     private RankingMap mLatestRankingMap;
@@ -144,7 +145,7 @@
     final ArrayList<NotificationLifetimeExtender> mNotificationLifetimeExtenders
             = new ArrayList<>();
     private final List<NotificationEntryListener> mNotificationEntryListeners = new ArrayList<>();
-    private NotificationRemoveInterceptor mRemoveInterceptor;
+    private final List<NotificationRemoveInterceptor> mRemoveInterceptors = new ArrayList<>();
 
     @Override
     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
@@ -157,6 +158,14 @@
                 pw.println(entry.getSbn());
             }
         }
+        pw.println("  Remove interceptors registered:");
+        for (NotificationRemoveInterceptor interceptor : mRemoveInterceptors) {
+            pw.println("    " + interceptor.getClass().getSimpleName());
+        }
+        pw.println("  Lifetime extenders registered:");
+        for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
+            pw.println("    " + extender.getClass().getSimpleName());
+        }
         pw.println("  Lifetime-extended notifications:");
         if (mRetainedNotifications.isEmpty()) {
             pw.println("    None");
@@ -178,7 +187,8 @@
             FeatureFlags featureFlags,
             Lazy<NotificationRowBinder> notificationRowBinderLazy,
             Lazy<NotificationRemoteInputManager> notificationRemoteInputManagerLazy,
-            LeakDetector leakDetector) {
+            LeakDetector leakDetector,
+            ForegroundServiceDismissalFeatureController fgsFeatureController) {
         mNotifLog = notifLog;
         mGroupManager = groupManager;
         mRankingManager = rankingManager;
@@ -187,6 +197,7 @@
         mNotificationRowBinderLazy = notificationRowBinderLazy;
         mRemoteInputManagerLazy = notificationRemoteInputManagerLazy;
         mLeakDetector = leakDetector;
+        mFgsFeatureController = fgsFeatureController;
     }
 
     /** Once called, the NEM will start processing notification events from system server. */
@@ -207,9 +218,14 @@
         mNotificationEntryListeners.remove(listener);
     }
 
-    /** Sets the {@link NotificationRemoveInterceptor}. */
-    public void setNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) {
-        mRemoveInterceptor = interceptor;
+    /** Add a {@link NotificationRemoveInterceptor}. */
+    public void addNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) {
+        mRemoveInterceptors.add(interceptor);
+    }
+
+    /** Remove a {@link NotificationRemoveInterceptor} */
+    public void removeNotificationRemoveInterceptor(NotificationRemoveInterceptor interceptor) {
+        mRemoveInterceptors.remove(interceptor);
     }
 
     public void setUpWithPresenter(NotificationPresenter presenter,
@@ -398,14 +414,16 @@
             boolean removedByUser,
             int reason) {
 
-        if (mRemoveInterceptor != null
-                && mRemoveInterceptor.onNotificationRemoveRequested(key, reason)) {
-            // Remove intercepted; log and skip
-            mNotifLog.log(NotifEvent.REMOVE_INTERCEPTED);
-            return;
+        final NotificationEntry entry = getActiveNotificationUnfiltered(key);
+
+        for (NotificationRemoveInterceptor interceptor : mRemoveInterceptors) {
+            if (interceptor.onNotificationRemoveRequested(key, entry, reason)) {
+                // Remove intercepted; log and skip
+                mNotifLog.log(NotifEvent.REMOVE_INTERCEPTED);
+                return;
+            }
         }
 
-        final NotificationEntry entry = getActiveNotificationUnfiltered(key);
         boolean lifetimeExtended = false;
 
         // Notification was canceled before it got inflated
@@ -528,7 +546,10 @@
         Ranking ranking = new Ranking();
         rankingMap.getRanking(key, ranking);
 
-        NotificationEntry entry = new NotificationEntry(notification, ranking);
+        NotificationEntry entry = new NotificationEntry(
+                notification,
+                ranking,
+                mFgsFeatureController.isForegroundServiceDismissalEnabled());
 
         mLeakDetector.trackInstance(entry);
 
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
index a95668b..df65dac 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/NotificationEntry.java
@@ -22,6 +22,7 @@
 import static android.app.Notification.CATEGORY_MESSAGE;
 import static android.app.Notification.CATEGORY_REMINDER;
 import static android.app.Notification.FLAG_BUBBLE;
+import static android.app.Notification.FLAG_FOREGROUND_SERVICE;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT;
@@ -177,18 +178,29 @@
     private Runnable mOnSensitiveChangedListener;
     private boolean mAutoHeadsUp;
     private boolean mPulseSupressed;
+    private boolean mAllowFgsDismissal;
     private int mBucket = BUCKET_ALERTING;
 
     public NotificationEntry(
             @NonNull StatusBarNotification sbn,
             @NonNull Ranking ranking) {
-        super(requireNonNull(requireNonNull(sbn).getKey()));
+        this(sbn, ranking, false);
+    }
+
+    public NotificationEntry(
+            @NonNull StatusBarNotification sbn,
+            @NonNull Ranking ranking,
+            boolean allowFgsDismissal
+    ) {
+        super(requireNonNull(Objects.requireNonNull(sbn).getKey()));
 
         requireNonNull(ranking);
 
         mKey = sbn.getKey();
         setSbn(sbn);
         setRanking(ranking);
+
+        mAllowFgsDismissal = allowFgsDismissal;
     }
 
     @Override
@@ -827,8 +839,11 @@
      *         notification can be dismissed in case notifications are sensitive on the lockscreen.
      * @see #canViewBeDismissed()
      */
+    // TOOD: This logic doesn't belong on NotificationEntry. It should be moved to the
+    // ForegroundsServiceDismissalFeatureController or some other controller that can be added
+    // as a dependency to any class that needs to answer this question.
     public boolean isClearable() {
-        if (!mSbn.isClearable()) {
+        if (!isDismissable()) {
             return false;
         }
 
@@ -836,7 +851,7 @@
         if (children != null && children.size() > 0) {
             for (int i = 0; i < children.size(); i++) {
                 NotificationEntry child =  children.get(i);
-                if (!child.isClearable()) {
+                if (!child.isDismissable()) {
                     return false;
                 }
             }
@@ -844,6 +859,31 @@
         return true;
     }
 
+    /**
+     * Notifications might have any combination of flags:
+     * - FLAG_ONGOING_EVENT
+     * - FLAG_NO_CLEAR
+     * - FLAG_FOREGROUND_SERVICE
+     *
+     * We want to allow dismissal of notifications that represent foreground services, which may
+     * have all 3 flags set. If we only find NO_CLEAR though, we don't want to allow dismissal
+     */
+    private boolean isDismissable() {
+        boolean ongoing = ((mSbn.getNotification().flags & Notification.FLAG_ONGOING_EVENT) != 0);
+        boolean noclear = ((mSbn.getNotification().flags & Notification.FLAG_NO_CLEAR) != 0);
+        boolean fgs = ((mSbn.getNotification().flags & FLAG_FOREGROUND_SERVICE) != 0);
+
+        if (mAllowFgsDismissal) {
+            if (noclear && !ongoing && !fgs) {
+                return false;
+            }
+            return true;
+        } else {
+            return mSbn.isClearable();
+        }
+
+    }
+
     public boolean canViewBeDismissed() {
         if (row == null) return true;
         return row.canViewBeDismissed();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/DungeonRow.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/DungeonRow.kt
new file mode 100644
index 0000000..373457d
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/DungeonRow.kt
@@ -0,0 +1,43 @@
+/*
+* Copyright (C) 2020 The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+*      http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+
+package com.android.systemui.statusbar.notification.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.R
+import com.android.systemui.statusbar.StatusBarIconView
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+
+class DungeonRow(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
+    var entry: NotificationEntry? = null
+        set(value) {
+            field = value
+            update()
+        }
+
+    private fun update() {
+        (findViewById(R.id.app_name) as TextView).apply {
+            text = entry?.row?.appName
+        }
+
+        (findViewById(R.id.icon) as StatusBarIconView).apply {
+            set(entry?.icon?.statusBarIcon)
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index b71beda..253be2fc 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -745,6 +745,11 @@
         mPrivateLayout.setRemoteInputController(r);
     }
 
+
+    String getAppName() {
+        return mAppName;
+    }
+
     public void addChildNotification(ExpandableNotificationRow row) {
         addChildNotification(row, -1);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ForegroundServiceDungeonView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ForegroundServiceDungeonView.kt
new file mode 100644
index 0000000..17396ad
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ForegroundServiceDungeonView.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.row
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+
+import com.android.systemui.R
+
+class ForegroundServiceDungeonView(context: Context, attrs: AttributeSet)
+    : StackScrollerDecorView(context, attrs) {
+    override fun findContentView(): View? {
+        return findViewById(R.id.foreground_service_dungeon)
+    }
+
+    override fun findSecondaryView(): View? {
+        return null
+    }
+
+    override fun setVisible(visible: Boolean, animate: Boolean) {
+        // Visibility is controlled by the ForegroundServiceSectionController
+    }
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ForegroundServiceSectionController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ForegroundServiceSectionController.kt
new file mode 100644
index 0000000..5757fe8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ForegroundServiceSectionController.kt
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack
+
+import android.content.Context
+import android.service.notification.NotificationListenerService.REASON_APP_CANCEL
+import android.service.notification.NotificationListenerService.REASON_APP_CANCEL_ALL
+import android.service.notification.NotificationListenerService.REASON_CANCEL
+import android.service.notification.NotificationListenerService.REASON_CANCEL_ALL
+import android.service.notification.NotificationListenerService.REASON_CLICK
+import android.service.notification.NotificationListenerService.REASON_GROUP_SUMMARY_CANCELED
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.LinearLayout
+
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.R
+import com.android.systemui.statusbar.notification.ForegroundServiceDismissalFeatureController
+import com.android.systemui.statusbar.notification.NotificationEntryListener
+import com.android.systemui.statusbar.notification.NotificationEntryManager
+import com.android.systemui.statusbar.notification.row.DungeonRow
+import com.android.systemui.util.Assert
+
+import javax.inject.Inject
+import javax.inject.Singleton
+
+/**
+ * Controller for the bottom area of NotificationStackScrollLayout. It owns swiped-away foreground
+ * service notifications and can reinstantiate them when requested.
+ */
+@Singleton
+class ForegroundServiceSectionController @Inject constructor(
+    val entryManager: NotificationEntryManager,
+    val featureController: ForegroundServiceDismissalFeatureController
+) {
+    private val TAG = "FgsSectionController"
+    private var context: Context? = null
+
+    private val entries = mutableSetOf<NotificationEntry>()
+
+    private var entriesView: View? = null
+
+    init {
+        if (featureController.isForegroundServiceDismissalEnabled()) {
+            entryManager.addNotificationRemoveInterceptor(this::shouldInterceptRemoval)
+
+            entryManager.addNotificationEntryListener(object : NotificationEntryListener {
+                override fun onPostEntryUpdated(entry: NotificationEntry) {
+                    if (entries.contains(entry)) {
+                        removeEntry(entry)
+                        addEntry(entry)
+                        update()
+                    }
+                }
+            })
+        }
+    }
+
+    private fun shouldInterceptRemoval(
+        key: String,
+        entry: NotificationEntry?,
+        reason: Int
+    ): Boolean {
+        Assert.isMainThread()
+        val isClearAll = reason == REASON_CANCEL_ALL
+        val isUserDismiss = reason == REASON_CANCEL || reason == REASON_CLICK
+        val isAppCancel = reason == REASON_APP_CANCEL || reason == REASON_APP_CANCEL_ALL
+        val isSummaryCancel = reason == REASON_GROUP_SUMMARY_CANCELED
+
+        if (entry == null) return false
+
+        // We only want to retain notifications that the user dismissed
+        // TODO: centralize the entry.isClearable logic and this so that it's clear when a notif is
+        // clearable
+        if (isUserDismiss && !entry.sbn.isClearable) {
+            if (!hasEntry(entry)) {
+                addEntry(entry)
+                update()
+            }
+            // TODO: This isn't ideal. Slightly better would at least be to have NEM update the
+            // notif list when an entry gets intercepted
+            entryManager.updateNotifications(
+                    "FgsSectionController.onNotificationRemoveRequested")
+            return true
+        } else if ((isClearAll || isSummaryCancel) && !entry.sbn.isClearable) {
+            // In the case where a FGS notification is part of a group that is cleared or a clear
+            // all, we actually want to stop its removal but also not put it into the dungeon
+            return true
+        } else if (hasEntry(entry)) {
+            removeEntry(entry)
+            update()
+            return false
+        }
+
+        return false
+    }
+
+    private fun removeEntry(entry: NotificationEntry) {
+        Assert.isMainThread()
+        entries.remove(entry)
+    }
+
+    private fun addEntry(entry: NotificationEntry) {
+        Assert.isMainThread()
+        entries.add(entry)
+    }
+
+    fun hasEntry(entry: NotificationEntry): Boolean {
+        Assert.isMainThread()
+        return entries.contains(entry)
+    }
+
+    fun initialize(context: Context) {
+        this.context = context
+    }
+
+    fun createView(li: LayoutInflater): View {
+        entriesView = li.inflate(R.layout.foreground_service_dungeon, null)
+        // Start out gone
+        entriesView!!.visibility = View.GONE
+        return entriesView!!
+    }
+
+    private fun update() {
+        Assert.isMainThread()
+        if (entriesView == null) {
+            throw IllegalStateException("ForegroundServiceSectionController is trying to show " +
+                    "dismissed fgs notifications without having been initialized!")
+        }
+
+        // TODO: these views should be recycled and not inflating on the main thread
+        (entriesView!!.findViewById(R.id.entry_list) as LinearLayout).apply {
+            removeAllViews()
+            entries.sortedBy { it.ranking.rank }.forEach { entry ->
+                val child = LayoutInflater.from(context)
+                        .inflate(R.layout.foreground_service_dungeon_row, null) as DungeonRow
+
+                child.entry = entry
+                child.setOnClickListener {
+                    removeEntry(child.entry!!)
+                    update()
+                    entry.row.unDismiss()
+                    entry.row.resetTranslation()
+                    entryManager.updateNotifications("ForegroundServiceSectionController.onClick")
+                }
+
+                addView(child)
+            }
+        }
+
+        if (entries.isEmpty()) {
+            entriesView?.visibility = View.GONE
+        } else {
+            entriesView?.visibility = View.VISIBLE
+        }
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
index 84a293e..4b9976c 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java
@@ -35,7 +35,6 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.Intent;
-import android.content.pm.UserInfo;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.graphics.Canvas;
@@ -57,7 +56,6 @@
 import android.util.Log;
 import android.util.MathUtils;
 import android.util.Pair;
-import android.util.SparseArray;
 import android.view.ContextThemeWrapper;
 import android.view.InputDevice;
 import android.view.LayoutInflater;
@@ -109,6 +107,7 @@
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
 import com.android.systemui.statusbar.notification.FakeShadowView;
+import com.android.systemui.statusbar.notification.ForegroundServiceDismissalFeatureController;
 import com.android.systemui.statusbar.notification.NotificationEntryListener;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationUtils;
@@ -121,6 +120,7 @@
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
 import com.android.systemui.statusbar.notification.row.FooterView;
+import com.android.systemui.statusbar.notification.row.ForegroundServiceDungeonView;
 import com.android.systemui.statusbar.notification.row.NotificationBlockingHelperManager;
 import com.android.systemui.statusbar.notification.row.NotificationGuts;
 import com.android.systemui.statusbar.notification.row.NotificationGutsManager;
@@ -506,6 +506,8 @@
 
     private final NotificationGutsManager mNotificationGutsManager;
     private final NotificationSectionsManager mSectionsManager;
+    private final ForegroundServiceSectionController mFgsSectionController;
+    private ForegroundServiceDungeonView mFgsSectionView;
     private boolean mAnimateBottomOnLayout;
     private float mLastSentAppear;
     private float mLastSentExpandedHeight;
@@ -525,7 +527,10 @@
             NotificationLockscreenUserManager notificationLockscreenUserManager,
             NotificationGutsManager notificationGutsManager,
             ZenModeController zenController,
-            NotificationSectionsManager notificationSectionsManager) {
+            NotificationSectionsManager notificationSectionsManager,
+            ForegroundServiceSectionController fgsSectionController,
+            ForegroundServiceDismissalFeatureController fgsFeatureController
+    ) {
         super(context, attrs, 0, 0);
         Resources res = getResources();
 
@@ -541,6 +546,7 @@
         mKeyguardBypassController = keyguardBypassController;
         mFalsingManager = falsingManager;
         mZenController = zenController;
+        mFgsSectionController = fgsSectionController;
 
         mSectionsManager = notificationSectionsManager;
         mSectionsManager.initialize(this, LayoutInflater.from(context));
@@ -614,6 +620,17 @@
         dynamicPrivacyController.addListener(this);
         mDynamicPrivacyController = dynamicPrivacyController;
         mStatusbarStateController = statusBarStateController;
+        initializeForegroundServiceSection(fgsFeatureController);
+    }
+
+    private void initializeForegroundServiceSection(
+            ForegroundServiceDismissalFeatureController featureController) {
+        if (featureController.isForegroundServiceDismissalEnabled()) {
+            LayoutInflater li = LayoutInflater.from(mContext);
+            mFgsSectionView =
+                    (ForegroundServiceDungeonView) mFgsSectionController.createView(li);
+            addView(mFgsSectionView, -1);
+        }
     }
 
     private void updateDismissRtlSetting(boolean dismissRtl) {
@@ -3374,7 +3391,7 @@
         if (currentIndex == -1) {
             boolean isTransient = false;
             if (child instanceof ExpandableNotificationRow
-                    && ((ExpandableNotificationRow) child).getTransientContainer() != null) {
+                    && child.getTransientContainer() != null) {
                 isTransient = true;
             }
             Log.e(TAG, "Attempting to re-position "
@@ -3387,10 +3404,10 @@
 
         if (child != null && child.getParent() == this && currentIndex != newIndex) {
             mChangePositionInProgress = true;
-            ((ExpandableView) child).setChangingPosition(true);
+            child.setChangingPosition(true);
             removeView(child);
             addView(child, newIndex);
-            ((ExpandableView) child).setChangingPosition(false);
+            child.setChangingPosition(false);
             mChangePositionInProgress = false;
             if (mIsExpanded && mAnimationsEnabled && child.getVisibility() != View.GONE) {
                 mChildrenChangingPositions.add(child);
@@ -5637,15 +5654,17 @@
      */
     @ShadeViewRefactor(RefactorComponent.STATE_RESOLVER)
     public void onUpdateRowStates() {
-        changeViewPosition(mFooterView, -1);
 
         // The following views will be moved to the end of mStackScroller. This counter represents
         // the offset from the last child. Initialized to 1 for the very last position. It is post-
         // incremented in the following "changeViewPosition" calls so that its value is correct for
         // subsequent calls.
         int offsetFromEnd = 1;
-        changeViewPosition(mEmptyShadeView,
-                getChildCount() - offsetFromEnd++);
+        if (mFgsSectionView != null) {
+            changeViewPosition(mFgsSectionView, getChildCount() - offsetFromEnd++);
+        }
+        changeViewPosition(mFooterView, getChildCount() - offsetFromEnd++);
+        changeViewPosition(mEmptyShadeView, getChildCount() - offsetFromEnd++);
 
         // No post-increment for this call because it is the last one. Make sure to add one if
         // another "changeViewPosition" call is ever added.
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
index 333b4a7..dcaf4ec 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/bubbles/BubbleControllerTest.java
@@ -237,7 +237,7 @@
         mEntryListener = mEntryListenerCaptor.getValue();
         // And the remove interceptor
         verify(mNotificationEntryManager, atLeastOnce())
-                .setNotificationRemoveInterceptor(mRemoveInterceptorCaptor.capture());
+                .addNotificationRemoveInterceptor(mRemoveInterceptorCaptor.capture());
         mRemoveInterceptor = mRemoveInterceptorCaptor.getValue();
     }
 
@@ -581,7 +581,7 @@
 
         // Simulate notification cancellation.
         mRemoveInterceptor.onNotificationRemoveRequested(
-                mRow.getEntry().getKey(), REASON_APP_CANCEL);
+                mRow.getEntry().getKey(), mRow.getEntry(), REASON_APP_CANCEL);
 
         mBubbleController.expandStackAndSelectBubble(key);
     }
@@ -649,7 +649,7 @@
         assertTrue(mBubbleController.hasBubbles());
 
         boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
-                mRow.getEntry().getKey(), REASON_APP_CANCEL);
+                mRow.getEntry().getKey(), mRow.getEntry(), REASON_APP_CANCEL);
 
         // Cancels always remove so no need to intercept
         assertFalse(intercepted);
@@ -666,7 +666,7 @@
                 mRow.getEntry()));
 
         boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
-                mRow.getEntry().getKey(), REASON_CANCEL_ALL);
+                mRow.getEntry().getKey(), mRow.getEntry(), REASON_CANCEL_ALL);
 
         // Intercept!
         assertTrue(intercepted);
@@ -689,7 +689,7 @@
                 mRow.getEntry()));
 
         boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
-                mRow.getEntry().getKey(), REASON_CANCEL);
+                mRow.getEntry().getKey(), mRow.getEntry(), REASON_CANCEL);
 
         // Intercept!
         assertTrue(intercepted);
@@ -718,7 +718,7 @@
 
         // Dismiss the notification
         boolean intercepted = mRemoveInterceptor.onNotificationRemoveRequested(
-                mRow.getEntry().getKey(), REASON_CANCEL);
+                mRow.getEntry().getKey(), mRow.getEntry(), REASON_CANCEL);
 
         // It's no longer a bubble so we shouldn't intercept
         assertFalse(intercepted);
@@ -736,7 +736,8 @@
         assertFalse(mBubbleController.isBubbleNotificationSuppressedFromShade(
                 mRow.getEntry()));
 
-        mRemoveInterceptor.onNotificationRemoveRequested(mRow.getEntry().getKey(), REASON_CANCEL);
+        mRemoveInterceptor.onNotificationRemoveRequested(
+                mRow.getEntry().getKey(), mRow.getEntry(), REASON_CANCEL);
 
         // Should update show in shade state
         assertTrue(mBubbleController.isBubbleNotificationSuppressedFromShade(
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
index c97813d..63c911b5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationViewHierarchyManagerTest.java
@@ -49,6 +49,7 @@
 import com.android.systemui.statusbar.notification.logging.NotificationLogger;
 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
 import com.android.systemui.statusbar.notification.row.ExpandableView;
+import com.android.systemui.statusbar.notification.stack.ForegroundServiceSectionController;
 import com.android.systemui.statusbar.notification.stack.NotificationListContainer;
 import com.android.systemui.statusbar.phone.KeyguardBypassController;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
@@ -104,7 +105,8 @@
                 mock(StatusBarStateControllerImpl.class), mEntryManager,
                 mock(KeyguardBypassController.class),
                 mock(BubbleController.class),
-                mock(DynamicPrivacyController.class));
+                mock(DynamicPrivacyController.class),
+                mock(ForegroundServiceSectionController.class));
         mViewHierarchyManager.setUpWithPresenter(mPresenter, mListContainer);
     }
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
index 296d0cef..20c67fa 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationEntryManagerTest.java
@@ -247,11 +247,12 @@
                 mFeatureFlags,
                 () -> notificationRowBinder,
                 () -> mRemoteInputManager,
-                mLeakDetector
+                mLeakDetector,
+                mock(ForegroundServiceDismissalFeatureController.class)
         );
         mEntryManager.setUpWithPresenter(mPresenter, mListContainer, mHeadsUpManager);
         mEntryManager.addNotificationEntryListener(mEntryListener);
-        mEntryManager.setNotificationRemoveInterceptor(mRemoveInterceptor);
+        mEntryManager.addNotificationRemoveInterceptor(mRemoveInterceptor);
 
         notificationRowBinder.setUpWithPresenter(
                 mPresenter, mListContainer, mHeadsUpManager, mBindCallback);
@@ -546,7 +547,8 @@
         mEntryManager.addActiveNotificationForTest(mEntry);
 
         // GIVEN interceptor that intercepts that entry
-        when(mRemoveInterceptor.onNotificationRemoveRequested(eq(mEntry.getKey()), anyInt()))
+        when(mRemoveInterceptor.onNotificationRemoveRequested(
+                eq(mEntry.getKey()), eq(mEntry), anyInt()))
                 .thenReturn(true);
 
         // WHEN the notification is removed
@@ -564,7 +566,8 @@
         mEntryManager.addActiveNotificationForTest(mEntry);
 
         // GIVEN interceptor that doesn't intercept
-        when(mRemoveInterceptor.onNotificationRemoveRequested(eq(mEntry.getKey()), anyInt()))
+        when(mRemoveInterceptor.onNotificationRemoveRequested(
+                eq(mEntry.getKey()), eq(mEntry), anyInt()))
                 .thenReturn(false);
 
         // WHEN the notification is removed
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/TestableNotificationEntryManager.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/TestableNotificationEntryManager.kt
index 29ce9207..7431459 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/TestableNotificationEntryManager.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/TestableNotificationEntryManager.kt
@@ -41,9 +41,10 @@
     ff: FeatureFlags,
     rb: dagger.Lazy<NotificationRowBinder>,
     notificationRemoteInputManagerLazy: dagger.Lazy<NotificationRemoteInputManager>,
-    leakDetector: LeakDetector
+    leakDetector: LeakDetector,
+    fgsFeatureController: ForegroundServiceDismissalFeatureController
 ) : NotificationEntryManager(log, gm, rm, ke, ff, rb,
-        notificationRemoteInputManagerLazy, leakDetector) {
+        notificationRemoteInputManagerLazy, leakDetector, fgsFeatureController) {
 
     public var countDownLatch: CountDownLatch = CountDownLatch(1)
 
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 5bd5638..70d76f0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -63,6 +63,7 @@
 import com.android.systemui.statusbar.StatusBarState;
 import com.android.systemui.statusbar.SysuiStatusBarStateController;
 import com.android.systemui.statusbar.notification.DynamicPrivacyController;
+import com.android.systemui.statusbar.notification.ForegroundServiceDismissalFeatureController;
 import com.android.systemui.statusbar.notification.NotificationEntryManager;
 import com.android.systemui.statusbar.notification.NotificationFilter;
 import com.android.systemui.statusbar.notification.NotificationSectionsFeatureManager;
@@ -178,7 +179,9 @@
                 mock(FeatureFlags.class),
                 () -> mock(NotificationRowBinder.class),
                 () -> mRemoteInputManager,
-                mock(LeakDetector.class));
+                mock(LeakDetector.class),
+                mock(ForegroundServiceDismissalFeatureController.class)
+        );
         mDependency.injectTestDependency(NotificationEntryManager.class, mEntryManager);
         mEntryManager.setUpForTest(mock(NotificationPresenter.class), null, mHeadsUpManager);
 
@@ -203,7 +206,10 @@
                 mLockscreenUserManager,
                 mock(NotificationGutsManager.class),
                 mZenModeController,
-                mNotificationSectionsManager);
+                mNotificationSectionsManager,
+                mock(ForegroundServiceSectionController.class),
+                mock(ForegroundServiceDismissalFeatureController.class)
+        );
         verify(mLockscreenUserManager).addUserChangedListener(userChangedCaptor.capture());
         mUserChangedListener = userChangedCaptor.getValue();
         mStackScroller = spy(mStackScrollerInternal);
@@ -394,8 +400,11 @@
 
         mStackScroller.onUpdateRowStates();
 
+        // Expecting the footer to be the last child
+        int expected = mStackScroller.getChildCount() - 1;
+
         // move footer to end
-        verify(mStackScroller).changeViewPosition(any(FooterView.class), eq(-1 /* end */));
+        verify(mStackScroller).changeViewPosition(any(FooterView.class), eq(expected));
     }
 
     @Test