summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtender.java89
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java65
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java65
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtenderTest.java88
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java48
-rw-r--r--services/core/java/com/android/server/notification/NotificationManagerService.java11
-rw-r--r--services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java14
7 files changed, 377 insertions, 3 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..c1a06a2efc49
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtender.java
@@ -0,0 +1,89 @@
+/*
+ * 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
+ 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());
+
+ 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) {
+ 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/NotificationEntryManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java
index 06f26c9cbc7c..148be512c5b7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java
@@ -38,6 +38,7 @@ import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.ArraySet;
+import android.util.ArrayMap;
import android.util.EventLog;
import android.util.Log;
import android.view.View;
@@ -71,6 +72,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
+import java.util.Map;
/**
* NotificationEntryManager is responsible for the adding, removing, and updating of notifications.
@@ -116,6 +118,12 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
private final SmartReplyController mSmartReplyController =
Dependency.get(SmartReplyController.class);
+ // A lifetime extender that watches for foreground service notifications
+ private final NotificationLifetimeExtender mFGSExtender =
+ new ForegroundServiceLifetimeExtender();
+ private final Map<NotificationData.Entry, NotificationLifetimeExtender> mRetainedNotifications =
+ new ArrayMap<>();
+
protected IStatusBarService mBarService;
protected NotificationPresenter mPresenter;
protected Callback mCallback;
@@ -227,6 +235,16 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
pw.println(entry.notification);
}
}
+ 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(" mUseHeadsUp=");
pw.println(mUseHeadsUp);
pw.print(" mKeysKeptForRemoteInput: ");
@@ -241,6 +259,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
mMessagingUtil = new NotificationMessagingUtil(context);
mSystemServicesProxy = SystemServicesProxy.getInstance(mContext);
mGroupManager.setPendingEntries(mPendingNotifications);
+ mFGSExtender.setCallback(key -> removeNotification(key, mLatestRankingMap));
}
public void setUpWithPresenter(NotificationPresenter presenter,
@@ -289,6 +308,12 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
mOnAppOpsClickListener = mGutsManager::openGuts;
}
+ @VisibleForTesting
+ protected Map<NotificationData.Entry, NotificationLifetimeExtender>
+ getRetainedNotificationMap() {
+ return mRetainedNotifications;
+ }
+
public NotificationData getNotificationData() {
return mNotificationData;
}
@@ -474,8 +499,17 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
@Override
public void removeNotification(String key, NotificationListenerService.RankingMap ranking) {
- boolean deferRemoval = false;
+ // First chance to extend the lifetime of a notification
+ NotificationData.Entry pendingEntry = mPendingNotifications.get(key);
+ if (pendingEntry != null) {
+ if (mFGSExtender.shouldExtendLifetimeForPendingNotification(pendingEntry)) {
+ extendLifetime(pendingEntry, mFGSExtender);
+ return;
+ }
+ }
+
abortExistingInflation(key);
+ boolean deferRemoval = false;
if (mHeadsUpManager.isHeadsUp(key)) {
// A cancel() in response to a remote input shouldn't be delayed, as it makes the
// sending look longer than it takes.
@@ -534,6 +568,11 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
}
}
+ if (entry != null && mFGSExtender.shouldExtendLifetime(entry)) {
+ extendLifetime(entry, mFGSExtender);
+ return;
+ }
+
// Actually removing notification so smart reply controller can forget about it.
mSmartReplyController.stopSending(entry);
@@ -569,9 +608,30 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
handleGroupSummaryRemoved(key);
StatusBarNotification old = removeNotificationViews(key, ranking);
+ // Make sure no lifetime extension is happening anymore
+ cancelLifetimeExtension(entry);
mCallback.onNotificationRemoved(key, old);
}
+ private void extendLifetime(
+ NotificationData.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);
+ }
+
+ private void cancelLifetimeExtension(NotificationData.Entry entry) {
+ NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry);
+ if (activeExtender != null) {
+ activeExtender.setShouldManageLifetime(entry, false);
+ }
+ }
+
public StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry,
CharSequence remoteInputText, boolean showSpinner) {
StatusBarNotification sbn = entry.notification;
@@ -855,6 +915,9 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
Log.w(TAG, "Notification that was kept for guts was updated. " + key);
}
+ // No need to keep the lifetime extension around if an update comes in
+ cancelLifetimeExtension(entry);
+
Notification n = notification.getNotification();
mNotificationData.updateRanking(ranking);
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..96cfab7aeec9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java
@@ -0,0 +1,65 @@
+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/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..daf2f95b1f98
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/ForegroundServiceLifetimeExtenderTest.java
@@ -0,0 +1,88 @@
+/*
+ * 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));
+ }
+} \ No newline at end of file
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java
index afe16cf13b76..69db4b5fa86d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java
@@ -16,6 +16,8 @@
package com.android.systemui.statusbar;
+import static com.android.systemui.statusbar.ForegroundServiceLifetimeExtender.MIN_FGS_TIME_MS;
+
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
@@ -54,11 +56,13 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.ForegroundServiceController;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.NotificationData.Entry;
import com.android.systemui.statusbar.notification.VisualStabilityManager;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import java.util.Map;
import junit.framework.Assert;
import org.junit.Before;
@@ -427,4 +431,48 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
Assert.assertTrue(newSbn.getNotification().extras
.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
}
+
+ @Test
+ public void testForegroundServiceNotificationKeptForFiveSeconds() {
+ // 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;
+ mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID,
+ 0, n, new UserHandle(ActivityManager.getCurrentUser()), null,
+ System.currentTimeMillis());
+ mEntry = new Entry(mSbn);
+ mEntryManager.getNotificationData().add(mEntry);
+ mEntryManager.removeNotification(mEntry.key, mRankingMap);
+
+ Map<NotificationData.Entry, NotificationLifetimeExtender> map =
+ mEntryManager.getRetainedNotificationMap();
+
+ Assert.assertTrue(map.containsKey(mEntry));
+ }
+
+ @Test
+ public void testForegroundServiceNotification_notRetainedIfShownForFiveSeconds() {
+ // 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;
+ mSbn = 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);
+ mEntry = new Entry(mSbn);
+ mEntryManager.getNotificationData().add(mEntry);
+ mEntryManager.removeNotification(mEntry.key, mRankingMap);
+
+ Map<NotificationData.Entry, NotificationLifetimeExtender> map =
+ mEntryManager.getRetainedNotificationMap();
+
+ Assert.assertFalse(map.containsKey(mEntry));
+ }
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index dfcbf0524c5d..897877ad729f 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -5658,8 +5658,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/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 3446c16cb32b..8b719de98f04 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -708,6 +708,20 @@ public class NotificationManagerServiceTest extends UiServiceTestCase {
}
@Test
+ public void testCancelImmediatelyAfterEnqueueNotifiesListeners_ForegroundServiceFlag()
+ throws Exception {
+ final StatusBarNotification sbn = generateNotificationRecord(null).sbn;
+ sbn.getNotification().flags =
+ Notification.FLAG_ONGOING_EVENT | FLAG_FOREGROUND_SERVICE;
+ mBinderService.enqueueNotificationWithTag(PKG, "opPkg", "tag",
+ sbn.getId(), sbn.getNotification(), sbn.getUserId());
+ mBinderService.cancelNotificationWithTag(PKG, "tag", sbn.getId(), sbn.getUserId());
+ waitForIdle();
+ verify(mListeners, times(1)).notifyPostedLocked(any(), any());
+ verify(mListeners, times(1)).notifyRemovedLocked(any(), anyInt(), any());
+ }
+
+ @Test
public void testUserInitiatedClearAll_noLeak() throws Exception {
final NotificationRecord n = generateNotificationRecord(
mTestNotificationChannel, 1, "group", true);