Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6b336a57 authored by Ned Burns's avatar Ned Burns Committed by Android (Google) Code Review
Browse files

Merge "Track and dump notification lifetime extension"

parents 5bfb2b7b a5dad556
Loading
Loading
Loading
Loading
+33 −7
Original line number Diff line number Diff line
@@ -47,6 +47,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.
@@ -64,6 +65,9 @@ public class NotificationEntryManager implements
    @VisibleForTesting
    protected final HashMap<String, NotificationEntry> mPendingNotifications = new HashMap<>();

    private final Map<NotificationEntry, NotificationLifetimeExtender> mRetainedNotifications =
            new ArrayMap<>();

    // Lazily retrieved dependencies
    private NotificationRemoteInputManager mRemoteInputManager;
    private NotificationRowBinder mNotificationRowBinder;
@@ -89,6 +93,16 @@ public class NotificationEntryManager implements
                pw.println(entry.notification);
            }
        }
        pw.println("  Lifetime-extended notifications:");
        if (mRetainedNotifications.isEmpty()) {
            pw.println("    None");
        } else {
            for (Map.Entry<NotificationEntry, NotificationLifetimeExtender> entry
                    : mRetainedNotifications.entrySet()) {
                pw.println("    " + entry.getKey().notification + " retained by "
                        + entry.getValue().getClass().getName());
            }
        }
    }

    public NotificationEntryManager(Context context) {
@@ -244,7 +258,7 @@ public class NotificationEntryManager implements
                for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
                    if (extender.shouldExtendLifetime(entry)) {
                        mLatestRankingMap = ranking;
                        extender.setShouldManageLifetime(entry, true /* shouldManage */);
                        extendLifetime(entry, extender);
                        lifetimeExtended = true;
                        break;
                    }
@@ -255,9 +269,7 @@ public class NotificationEntryManager implements
                // At this point, we are guaranteed the notification will be removed

                // Ensure any managers keeping the lifetime extended stop managing the entry
                for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
                    extender.setShouldManageLifetime(entry, false /* shouldManage */);
                }
                cancelLifetimeExtension(entry);

                if (entry.rowExists()) {
                    entry.removeRow();
@@ -368,9 +380,7 @@ public class NotificationEntryManager implements

        // Notification is updated so it is essentially re-added and thus alive again.  Don't need
        // to keep its lifetime extended.
        for (NotificationLifetimeExtender extender : mNotificationLifetimeExtenders) {
            extender.setShouldManageLifetime(entry, false /* shouldManage */);
        }
        cancelLifetimeExtension(entry);

        mNotificationData.update(entry, ranking, notification);

@@ -464,4 +474,20 @@ public class NotificationEntryManager implements
    public Iterable<NotificationEntry> getPendingNotificationsIterator() {
        return mPendingNotifications.values();
    }

    private void extendLifetime(NotificationEntry entry, NotificationLifetimeExtender extender) {
        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(NotificationEntry entry) {
        NotificationLifetimeExtender activeExtender = mRetainedNotifications.remove(entry);
        if (activeExtender != null) {
            activeExtender.setShouldManageLifetime(entry, false);
        }
    }
}
+136 −22
Original line number Diff line number Diff line
@@ -45,8 +45,11 @@ import android.service.notification.StatusBarNotification;
import android.support.test.filters.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.util.ArraySet;
import android.widget.FrameLayout;

import androidx.annotation.NonNull;

import com.android.internal.logging.MetricsLogger;
import com.android.systemui.Dependency;
import com.android.systemui.ForegroundServiceController;
@@ -84,6 +87,7 @@ import org.mockito.MockitoAnnotations;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;

@@ -347,28 +351,6 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
        assertNull(mEntryManager.getNotificationData().get(mSbn.getKey()));
    }

    @Test
    public void testRemoveNotification_blockedByLifetimeExtender() {
        com.android.systemui.util.Assert.isNotMainThread();

        NotificationLifetimeExtender extender = mock(NotificationLifetimeExtender.class);
        when(extender.shouldExtendLifetime(mEntry)).thenReturn(true);

        ArrayList<NotificationLifetimeExtender> extenders = mEntryManager.getLifetimeExtenders();
        extenders.clear();
        extenders.add(extender);

        mEntry.setRow(mRow);
        mEntryManager.getNotificationData().add(mEntry);

        mEntryManager.removeNotification(mSbn.getKey(), mRankingMap);

        assertNotNull(mEntryManager.getNotificationData().get(mSbn.getKey()));
        verify(extender).setShouldManageLifetime(mEntry, true /* shouldManage */);
        verify(mEntryListener, never()).onEntryRemoved(
                mEntry, null, false /* removedByUser */);
    }

    @Test
    public void testRemoveNotification_onEntryRemoveNotFiredIfEntryDoesntExist() {
        com.android.systemui.util.Assert.isNotMainThread();
@@ -453,10 +435,142 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
        assertEquals("action", mEntry.systemGeneratedSmartActions.get(0).title);
    }

    @Test
    public void testLifetimeExtenders_ifNotificationIsRetainedItIsntRemoved() {
        // GIVEN an entry manager with a notification
        mEntryManager.setRowBinder(mMockedRowBinder);
        mEntryManager.getNotificationData().add(mEntry);

        // GIVEN a lifetime extender that always tries to extend lifetime
        NotificationLifetimeExtender extender = mock(NotificationLifetimeExtender.class);
        when(extender.shouldExtendLifetime(mEntry)).thenReturn(true);
        mEntryManager.addNotificationLifetimeExtender(extender);

        // WHEN the notification is removed
        mEntryManager.removeNotification(mEntry.key, mRankingMap);

        // THEN the extender is asked to manage the lifetime
        verify(extender).setShouldManageLifetime(mEntry, true);
        // THEN the notification is retained
        assertNotNull(mEntryManager.getNotificationData().get(mSbn.getKey()));
        verify(mEntryListener, never()).onEntryRemoved(mEntry, null, false);
    }

    @Test
    public void testLifetimeExtenders_whenRetentionEndsNotificationIsRemoved() {
        // GIVEN an entry manager with a notification whose life has been extended
        mEntryManager.setRowBinder(mMockedRowBinder);
        mEntryManager.getNotificationData().add(mEntry);
        final FakeNotificationLifetimeExtender extender = new FakeNotificationLifetimeExtender();
        mEntryManager.addNotificationLifetimeExtender(extender);
        mEntryManager.removeNotification(mEntry.key, mRankingMap);
        assertTrue(extender.isManaging(mEntry.key));

        // WHEN the extender finishes its extension
        extender.setExtendLifetimes(false);
        extender.getCallback().onSafeToRemove(mEntry.key);

        // THEN the notification is removed
        assertNull(mEntryManager.getNotificationData().get(mSbn.getKey()));
        verify(mEntryListener).onEntryRemoved(mEntry, null, false);
    }

    @Test
    public void testLifetimeExtenders_whenNotificationUpdatedRetainersAreCanceled() {
        // GIVEN an entry manager with a notification whose life has been extended
        mEntryManager.setRowBinder(mMockedRowBinder);
        mEntryManager.getNotificationData().add(mEntry);
        NotificationLifetimeExtender extender = mock(NotificationLifetimeExtender.class);
        when(extender.shouldExtendLifetime(mEntry)).thenReturn(true);
        mEntryManager.addNotificationLifetimeExtender(extender);
        mEntryManager.removeNotification(mEntry.key, mRankingMap);

        // WHEN the notification is updated
        mEntryManager.updateNotification(mEntry.notification, mRankingMap);

        // THEN the lifetime extension is canceled
        verify(extender).setShouldManageLifetime(mEntry, false);
    }

    @Test
    public void testLifetimeExtenders_whenNewExtenderTakesPrecedenceOldExtenderIsCanceled() {
        // GIVEN an entry manager with a notification
        mEntryManager.setRowBinder(mMockedRowBinder);
        mEntryManager.getNotificationData().add(mEntry);

        // GIVEN two lifetime extenders, the first which never extends and the second which
        // always extends
        NotificationLifetimeExtender extender1 = mock(NotificationLifetimeExtender.class);
        when(extender1.shouldExtendLifetime(mEntry)).thenReturn(false);
        NotificationLifetimeExtender extender2 = mock(NotificationLifetimeExtender.class);
        when(extender2.shouldExtendLifetime(mEntry)).thenReturn(true);
        mEntryManager.addNotificationLifetimeExtender(extender1);
        mEntryManager.addNotificationLifetimeExtender(extender2);

        // GIVEN a notification was lifetime-extended and extender2 is managing it
        mEntryManager.removeNotification(mEntry.key, mRankingMap);
        verify(extender1, never()).setShouldManageLifetime(mEntry, true);
        verify(extender2).setShouldManageLifetime(mEntry, true);

        // WHEN the extender1 changes its mind and wants to extend the lifetime of the notif
        when(extender1.shouldExtendLifetime(mEntry)).thenReturn(true);
        mEntryManager.removeNotification(mEntry.key, mRankingMap);

        // THEN extender2 stops managing the notif and extender1 starts managing it
        verify(extender1).setShouldManageLifetime(mEntry, true);
        verify(extender2).setShouldManageLifetime(mEntry, false);
    }

    private Notification.Action createAction() {
        return new Notification.Action.Builder(
                Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon),
                "action",
                PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build();
    }

    private static class FakeNotificationLifetimeExtender implements NotificationLifetimeExtender {
        private NotificationSafeToRemoveCallback mCallback;
        private boolean mExtendLifetimes = true;
        private Set<String> mManagedNotifs = new ArraySet<>();

        @Override
        public void setCallback(@NonNull NotificationSafeToRemoveCallback callback) {
            mCallback = callback;
        }

        @Override
        public boolean shouldExtendLifetime(@NonNull NotificationEntry entry) {
            return mExtendLifetimes;
        }

        @Override
        public void setShouldManageLifetime(
                @NonNull NotificationEntry entry,
                boolean shouldManage) {
            final boolean hasEntry = mManagedNotifs.contains(entry.key);
            if (shouldManage) {
                if (hasEntry) {
                    throw new RuntimeException("Already managing this entry: " + entry.key);
                }
                mManagedNotifs.add(entry.key);
            } else {
                if (!hasEntry) {
                    throw new RuntimeException("Not managing this entry: " + entry.key);
                }
                mManagedNotifs.remove(entry.key);
            }
        }

        public void setExtendLifetimes(boolean extendLifetimes) {
            mExtendLifetimes = extendLifetimes;
        }

        public NotificationSafeToRemoveCallback getCallback() {
            return mCallback;
        }

        public boolean isManaging(String notificationKey) {
            return mManagedNotifs.contains(notificationKey);
        }
    }
}