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

Commit efc032f6 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Refactor managers that defer notification removal"

parents e643be00 a5ff1faf
Loading
Loading
Loading
Loading
+54 −4
Original line number Diff line number Diff line
@@ -36,10 +36,20 @@ import java.util.stream.Stream;
 * remove notifications that appear on screen for a period of time and dismiss themselves at the
 * appropriate time.  These include heads up notifications and ambient pulses.
 */
public abstract class AlertingNotificationManager {
public abstract class AlertingNotificationManager implements NotificationLifetimeExtender {
    private static final String TAG = "AlertNotifManager";
    protected final Clock mClock = new Clock();
    protected final ArrayMap<String, AlertEntry> mAlertEntries = new ArrayMap<>();

    /**
     * This is the list of entries that have already been removed from the
     * NotificationManagerService side, but we keep it to prevent the UI from looking weird and
     * will remove when possible. See {@link NotificationLifetimeExtender}
     */
    protected final ArraySet<NotificationData.Entry> mExtendedLifetimeAlertEntries =
            new ArraySet<>();

    protected NotificationSafeToRemoveCallback mNotificationLifetimeFinishedCallback;
    protected int mMinimumDisplayTime;
    protected int mAutoDismissNotificationDecay;
    @VisibleForTesting
@@ -74,7 +84,7 @@ public abstract class AlertingNotificationManager {
        if (alertEntry == null) {
            return true;
        }
        if (releaseImmediately || alertEntry.wasShownLongEnough()) {
        if (releaseImmediately || canRemoveImmediately(key)) {
            removeAlertEntry(key);
        } else {
            alertEntry.removeAsSoonAsPossible();
@@ -191,6 +201,12 @@ public abstract class AlertingNotificationManager {
        onAlertEntryRemoved(alertEntry);
        entry.row.sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
        alertEntry.reset();
        if (mExtendedLifetimeAlertEntries.contains(entry)) {
            if (mNotificationLifetimeFinishedCallback != null) {
                mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
            }
            mExtendedLifetimeAlertEntries.remove(entry);
        }
    }

    /**
@@ -207,6 +223,40 @@ public abstract class AlertingNotificationManager {
        return new AlertEntry();
    }

    /**
     * Whether or not the alert can be removed currently.  If it hasn't been on screen long enough
     * it should not be removed unless forced
     * @param key the key to check if removable
     * @return true if the alert entry can be removed
     */
    protected boolean canRemoveImmediately(String key) {
        AlertEntry alertEntry = mAlertEntries.get(key);
        return alertEntry == null || alertEntry.wasShownLongEnough();
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // NotificationLifetimeExtender Methods

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

    @Override
    public boolean shouldExtendLifetime(NotificationData.Entry entry) {
        return !canRemoveImmediately(entry.key);
    }

    @Override
    public void setShouldExtendLifetime(NotificationData.Entry entry, boolean shouldExtend) {
        if (shouldExtend) {
            mExtendedLifetimeAlertEntries.add(entry);
        } else {
            mExtendedLifetimeAlertEntries.remove(entry);
        }
    }
    ///////////////////////////////////////////////////////////////////////////////////////////////

    protected class AlertEntry implements Comparable<AlertEntry> {
        @Nullable public NotificationData.Entry mEntry;
        public long mPostTime;
@@ -214,11 +264,11 @@ public abstract class AlertingNotificationManager {

        @Nullable protected Runnable mRemoveAlertRunnable;

        public void setEntry(@Nullable final NotificationData.Entry entry) {
        public void setEntry(@NonNull final NotificationData.Entry entry) {
            setEntry(entry, () -> removeAlertEntry(entry.key));
        }

        public void setEntry(@Nullable final NotificationData.Entry entry,
        public void setEntry(@NonNull final NotificationData.Entry entry,
                @Nullable Runnable removeAlertRunnable) {
            mEntry = entry;
            mRemoveAlertRunnable = removeAlertRunnable;
+53 −0
Original line number Diff line number Diff line
package com.android.systemui.statusbar;

import com.android.systemui.statusbar.notification.NotificationData;

import androidx.annotation.NonNull;

/**
 * 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);

    /**
     * Sets whether or not the lifetime should be extended.  In practice, if shouldExtend 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 shouldExtend 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 to mark as having an extended lifetime
     * @param shouldExtend true if the extender should manage the entry now, false otherwise
     */
    void setShouldExtendLifetime(@NonNull NotificationData.Entry entry, boolean shouldExtend);

    /**
     * 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);
    }
}
+0 −1
Original line number Diff line number Diff line
@@ -76,7 +76,6 @@ public class NotificationListener extends NotificationListenerWithPlugins {
            mPresenter.getHandler().post(() -> {
                processForRemoteInput(sbn.getNotification(), mContext);
                String key = sbn.getKey();
                mEntryManager.removeKeyKeptForRemoteInput(key);
                boolean isUpdate =
                        mEntryManager.getNotificationData().get(key) != null;
                // In case we don't allow child notifications, we ignore children of
+261 −34
Original line number Diff line number Diff line
@@ -17,8 +17,10 @@ package com.android.systemui.statusbar;

import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY;

import android.annotation.NonNull;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
@@ -29,6 +31,7 @@ import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserManager;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import android.view.MotionEvent;
@@ -50,6 +53,7 @@ import com.android.systemui.statusbar.policy.RemoteInputView;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Set;

/**
@@ -61,10 +65,10 @@ import java.util.Set;
public class NotificationRemoteInputManager implements Dumpable {
    public static final boolean ENABLE_REMOTE_INPUT =
            SystemProperties.getBoolean("debug.enable_remote_input", true);
    public static final boolean FORCE_REMOTE_INPUT_HISTORY =
    public static boolean FORCE_REMOTE_INPUT_HISTORY =
            SystemProperties.getBoolean("debug.force_remoteinput_history", true);
    private static final boolean DEBUG = false;
    private static final String TAG = "NotificationRemoteInputManager";
    private static final String TAG = "NotifRemoteInputManager";

    /**
     * How long to wait before auto-dismissing a notification that was kept for remote input, and
@@ -74,12 +78,25 @@ public class NotificationRemoteInputManager implements Dumpable {
     */
    private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;

    protected final ArraySet<NotificationData.Entry> mRemoteInputEntriesToRemoveOnCollapse =
    /**
     * Notifications that are already removed but are kept around because we want to show the
     * remote input history. See {@link RemoteInputHistoryExtender} and
     * {@link SmartReplyHistoryExtender}.
     */
    protected final ArraySet<String> mKeysKeptForRemoteInputHistory = new ArraySet<>();

    /**
     * Notifications that are already removed but are kept around because the remote input is
     * actively being used (i.e. user is typing in it).  See {@link RemoteInputActiveExtender}.
     */
    protected final ArraySet<NotificationData.Entry> mEntriesKeptForRemoteInputActive =
            new ArraySet<>();

    // Dependencies:
    protected final NotificationLockscreenUserManager mLockscreenUserManager =
            Dependency.get(NotificationLockscreenUserManager.class);
    protected final SmartReplyController mSmartReplyController =
            Dependency.get(SmartReplyController.class);

    protected final Context mContext;
    private final UserManager mUserManager;
@@ -87,8 +104,11 @@ public class NotificationRemoteInputManager implements Dumpable {
    protected RemoteInputController mRemoteInputController;
    protected NotificationPresenter mPresenter;
    protected NotificationEntryManager mEntryManager;
    protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
            mNotificationLifetimeFinishedCallback;
    protected IStatusBarService mBarService;
    protected Callback mCallback;
    protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();

    private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() {

@@ -276,6 +296,7 @@ public class NotificationRemoteInputManager implements Dumpable {
        mBarService = IStatusBarService.Stub.asInterface(
                ServiceManager.getService(Context.STATUS_BAR_SERVICE));
        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
        addLifetimeExtenders();
    }

    public void setUpWithPresenter(NotificationPresenter presenter,
@@ -290,16 +311,16 @@ public class NotificationRemoteInputManager implements Dumpable {
            @Override
            public void onRemoteInputSent(NotificationData.Entry entry) {
                if (FORCE_REMOTE_INPUT_HISTORY
                        && mEntryManager.isNotificationKeptForRemoteInput(entry.key)) {
                    mEntryManager.removeNotification(entry.key, null);
                } else if (mRemoteInputEntriesToRemoveOnCollapse.contains(entry)) {
                        && isNotificationKeptForRemoteInputHistory(entry.key)) {
                    mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
                } else if (mEntriesKeptForRemoteInputActive.contains(entry)) {
                    // We're currently holding onto this notification, but from the apps point of
                    // view it is already canceled, so we'll need to cancel it on the apps behalf
                    // after sending - unless the app posts an update in the mean time, so wait a
                    // bit.
                    mPresenter.getHandler().postDelayed(() -> {
                        if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) {
                            mEntryManager.removeNotification(entry.key, null);
                        if (mEntriesKeptForRemoteInputActive.remove(entry)) {
                            mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
                        }
                    }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
                }
@@ -310,45 +331,74 @@ public class NotificationRemoteInputManager implements Dumpable {
                }
            }
        });

        mSmartReplyController.setCallback((entry, reply) -> {
            StatusBarNotification newSbn =
                    rebuildNotificationWithRemoteInput(entry, reply, true /* showSpinner */);
            mEntryManager.updateNotification(newSbn, null /* ranking */);
        });
    }

    public RemoteInputController getController() {
        return mRemoteInputController;
    /**
     * Adds all the notification lifetime extenders. Each extender represents a reason for the
     * NotificationRemoteInputManager to keep a notification lifetime extended.
     */
    protected void addLifetimeExtenders() {
        mLifetimeExtenders.add(new RemoteInputHistoryExtender());
        mLifetimeExtenders.add(new SmartReplyHistoryExtender());
        mLifetimeExtenders.add(new RemoteInputActiveExtender());
    }

    public void onUpdateNotification(NotificationData.Entry entry) {
        mRemoteInputEntriesToRemoveOnCollapse.remove(entry);
    public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
        return mLifetimeExtenders;
    }

    /**
     * Returns true if NotificationRemoteInputManager wants to keep this notification around.
     *
     * @param entry notification being removed
     */
    public boolean onRemoveNotification(NotificationData.Entry entry) {
        if (entry != null && mRemoteInputController.isRemoteInputActive(entry)
                && (entry.row != null && !entry.row.isDismissed())) {
            mRemoteInputEntriesToRemoveOnCollapse.add(entry);
            return true;
        }
        return false;
    public RemoteInputController getController() {
        return mRemoteInputController;
    }

    public void onPerformRemoveNotification(StatusBarNotification n,
            NotificationData.Entry entry) {
        if (mKeysKeptForRemoteInputHistory.contains(n.getKey())) {
            mKeysKeptForRemoteInputHistory.remove(n.getKey());
        }
        if (mRemoteInputController.isRemoteInputActive(entry)) {
            mRemoteInputController.removeRemoteInput(entry, null);
        }
    }

    public void removeRemoteInputEntriesKeptUntilCollapsed() {
        for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) {
            NotificationData.Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i);
    public void onPanelCollapsed() {
        for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
            NotificationData.Entry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
            mRemoteInputController.removeRemoteInput(entry, null);
            mEntryManager.removeNotification(entry.key, mEntryManager.getLatestRankingMap());
            if (mNotificationLifetimeFinishedCallback != null) {
                mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.key);
            }
        mRemoteInputEntriesToRemoveOnCollapse.clear();
        }
        mEntriesKeptForRemoteInputActive.clear();
    }

    public boolean isNotificationKeptForRemoteInputHistory(String key) {
        return mKeysKeptForRemoteInputHistory.contains(key);
    }

    public boolean shouldKeepForRemoteInputHistory(NotificationData.Entry entry) {
        if (entry.row == null || entry.row.isDismissed()) {
            return false;
        }
        if (!FORCE_REMOTE_INPUT_HISTORY) {
            return false;
        }
        return (mRemoteInputController.isSpinning(entry.key) || entry.hasJustSentRemoteInput());
    }

    public boolean shouldKeepForSmartReplyHistory(NotificationData.Entry entry) {
        if (entry.row == null || entry.row.isDismissed()) {
            return false;
        }
        if (!FORCE_REMOTE_INPUT_HISTORY) {
            return false;
        }
        return mSmartReplyController.isSendingSmartReply(entry.key);
    }

    public void checkRemoteInputOutside(MotionEvent event) {
@@ -359,11 +409,63 @@ public class NotificationRemoteInputManager implements Dumpable {
        }
    }

    @VisibleForTesting
    StatusBarNotification rebuildNotificationForCanceledSmartReplies(
            NotificationData.Entry entry) {
        return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
                false /* showSpinner */);
    }

    @VisibleForTesting
    StatusBarNotification rebuildNotificationWithRemoteInput(NotificationData.Entry entry,
            CharSequence remoteInputText, boolean showSpinner) {
        StatusBarNotification sbn = entry.notification;

        Notification.Builder b = Notification.Builder
                .recoverBuilder(mContext, sbn.getNotification().clone());
        if (remoteInputText != null) {
            CharSequence[] oldHistory = sbn.getNotification().extras
                    .getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY);
            CharSequence[] newHistory;
            if (oldHistory == null) {
                newHistory = new CharSequence[1];
            } else {
                newHistory = new CharSequence[oldHistory.length + 1];
                System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
            }
            newHistory[0] = String.valueOf(remoteInputText);
            b.setRemoteInputHistory(newHistory);
        }
        b.setShowRemoteInputSpinner(showSpinner);
        b.setHideSmartReplies(true);

        Notification newNotification = b.build();

        // Undo any compatibility view inflation
        newNotification.contentView = sbn.getNotification().contentView;
        newNotification.bigContentView = sbn.getNotification().bigContentView;
        newNotification.headsUpContentView = sbn.getNotification().headsUpContentView;

        return new StatusBarNotification(
                sbn.getPackageName(),
                sbn.getOpPkg(),
                sbn.getId(),
                sbn.getTag(),
                sbn.getUid(),
                sbn.getInitialPid(),
                newNotification,
                sbn.getUser(),
                sbn.getOverrideGroupKey(),
                sbn.getPostTime());
    }

    @Override
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("NotificationRemoteInputManager state:");
        pw.print("  mRemoteInputEntriesToRemoveOnCollapse: ");
        pw.println(mRemoteInputEntriesToRemoveOnCollapse);
        pw.print("  mKeysKeptForRemoteInputHistory: ");
        pw.println(mKeysKeptForRemoteInputHistory);
        pw.print("  mEntriesKeptForRemoteInputActive: ");
        pw.println(mEntriesKeptForRemoteInputActive);
    }

    public void bindRow(ExpandableNotificationRow row) {
@@ -372,8 +474,133 @@ public class NotificationRemoteInputManager implements Dumpable {
    }

    @VisibleForTesting
    public Set<NotificationData.Entry> getRemoteInputEntriesToRemoveOnCollapse() {
        return mRemoteInputEntriesToRemoveOnCollapse;
    public Set<NotificationData.Entry> getEntriesKeptForRemoteInputActive() {
        return mEntriesKeptForRemoteInputActive;
    }

    /**
     * NotificationRemoteInputManager has multiple reasons to keep notification lifetime extended
     * so we implement multiple NotificationLifetimeExtenders
     */
    protected abstract class RemoteInputExtender implements NotificationLifetimeExtender {
        @Override
        public void setCallback(NotificationSafeToRemoveCallback callback) {
            if (mNotificationLifetimeFinishedCallback == null) {
                mNotificationLifetimeFinishedCallback = callback;
            }
        }
    }

    /**
     * Notification is kept alive as it was cancelled in response to a remote input interaction.
     * This allows us to show what you replied and allows you to continue typing into it.
     */
    protected class RemoteInputHistoryExtender extends RemoteInputExtender {
        @Override
        public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
            return shouldKeepForRemoteInputHistory(entry);
        }

        @Override
        public void setShouldExtendLifetime(NotificationData.Entry entry,
                boolean shouldExtend) {
            if (shouldExtend) {
                CharSequence remoteInputText = entry.remoteInputText;
                if (TextUtils.isEmpty(remoteInputText)) {
                    remoteInputText = entry.remoteInputTextWhenReset;
                }
                StatusBarNotification newSbn = rebuildNotificationWithRemoteInput(entry,
                        remoteInputText, false /* showSpinner */);
                entry.onRemoteInputInserted();

                if (newSbn == null) {
                    return;
                }

                mEntryManager.updateNotification(newSbn, null);

                // Ensure the entry hasn't already been removed. This can happen if there is an
                // inflation exception while updating the remote history
                if (entry.row == null || entry.row.isRemoved()) {
                    return;
                }

                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Keeping notification around after sending remote input "
                            + entry.key);
                }

                mKeysKeptForRemoteInputHistory.add(entry.key);
            } else {
                mKeysKeptForRemoteInputHistory.remove(entry.key);
            }
        }
    }

    /**
     * Notification is kept alive for smart reply history.  Similar to REMOTE_INPUT_HISTORY but with
     * {@link SmartReplyController} specific logic
     */
    protected class SmartReplyHistoryExtender extends RemoteInputExtender {
        @Override
        public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
            return shouldKeepForSmartReplyHistory(entry);
        }

        @Override
        public void setShouldExtendLifetime(NotificationData.Entry entry,
                boolean shouldExtend) {
            if (shouldExtend) {
                StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);

                if (newSbn == null) {
                    return;
                }

                mEntryManager.updateNotification(newSbn, null);

                if (entry.row == null || entry.row.isRemoved()) {
                    return;
                }

                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Keeping notification around after sending smart reply "
                            + entry.key);
                }

                mKeysKeptForRemoteInputHistory.add(entry.key);
            } else {
                mKeysKeptForRemoteInputHistory.remove(entry.key);
                mSmartReplyController.stopSending(entry);
            }
        }
    }

    /**
     * Notification is kept alive because the user is still using the remote input
     */
    protected class RemoteInputActiveExtender extends RemoteInputExtender {
        @Override
        public boolean shouldExtendLifetime(@NonNull NotificationData.Entry entry) {
            if (entry.row == null || entry.row.isDismissed()) {
                return false;
            }
            return mRemoteInputController.isRemoteInputActive(entry);
        }

        @Override
        public void setShouldExtendLifetime(NotificationData.Entry entry,
                boolean shouldExtend) {
            if (shouldExtend) {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Keeping notification around while remote input active "
                            + entry.key);
                }
                mEntriesKeptForRemoteInputActive.add(entry);
            } else {
                mEntriesKeptForRemoteInputActive.remove(entry);
            }
        }
    }

    /**
+19 −7
Original line number Diff line number Diff line
@@ -33,20 +33,19 @@ import java.util.Set;
public class SmartReplyController {
    private IStatusBarService mBarService;
    private Set<String> mSendingKeys = new ArraySet<>();
    private Callback mCallback;

    public SmartReplyController() {
        mBarService = Dependency.get(IStatusBarService.class);
    }

    public void setCallback(Callback callback) {
        mCallback = callback;
    }

    public void smartReplySent(NotificationData.Entry entry, int replyIndex, CharSequence reply) {
        NotificationEntryManager notificationEntryManager
                = Dependency.get(NotificationEntryManager.class);
        StatusBarNotification newSbn =
                notificationEntryManager.rebuildNotificationWithRemoteInput(entry, reply,
                        true /* showSpinner */);
        notificationEntryManager.updateNotification(newSbn, null /* ranking */);
        mCallback.onSmartReplySent(entry, reply);
        mSendingKeys.add(entry.key);

        try {
            mBarService.onNotificationSmartReplySent(entry.notification.getKey(),
                    replyIndex);
@@ -77,4 +76,17 @@ public class SmartReplyController {
            mSendingKeys.remove(entry.notification.getKey());
        }
    }

    /**
     * Callback for any class that needs to do something in response to a smart reply being sent.
     */
    public interface Callback {
        /**
         * A smart reply has just been sent for a notification
         *
         * @param entry the entry for the notification
         * @param reply the reply that was sent
         */
        void onSmartReplySent(NotificationData.Entry entry, CharSequence reply);
    }
}
Loading