diff options
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 87841a5a413f..651260416d7c 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; @@ -238,6 +246,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: "); @@ -252,6 +270,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, @@ -300,6 +319,12 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.          mOnAppOpsClickListener = mGutsManager::openGuts;      } +    @VisibleForTesting +    protected Map<NotificationData.Entry, NotificationLifetimeExtender> +    getRetainedNotificationMap() { +        return mRetainedNotifications; +    } +      public NotificationData getNotificationData() {          return mNotificationData;      } @@ -485,8 +510,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. @@ -545,6 +579,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); @@ -580,9 +619,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; @@ -872,6 +932,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 1928cb7d0f0e..b1ea47c6f3b0 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -5744,8 +5744,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 5622622e925e..b76cadb45999 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -709,6 +709,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); |