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