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