Loading packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java +54 −4 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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(); Loading Loading @@ -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); } } /** Loading @@ -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; Loading @@ -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; Loading packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java 0 → 100644 +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); } } packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java +0 −1 Original line number Diff line number Diff line Loading @@ -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 Loading packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +261 −34 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; /** Loading @@ -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 Loading @@ -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; Loading @@ -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() { Loading Loading @@ -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, Loading @@ -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); } Loading @@ -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) { Loading @@ -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) { Loading @@ -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); } } } /** Loading packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java +19 −7 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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
packages/SystemUI/src/com/android/systemui/statusbar/AlertingNotificationManager.java +54 −4 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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(); Loading Loading @@ -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); } } /** Loading @@ -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; Loading @@ -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; Loading
packages/SystemUI/src/com/android/systemui/statusbar/NotificationLifetimeExtender.java 0 → 100644 +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); } }
packages/SystemUI/src/com/android/systemui/statusbar/NotificationListener.java +0 −1 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +261 −34 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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; Loading @@ -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; /** Loading @@ -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 Loading @@ -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; Loading @@ -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() { Loading Loading @@ -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, Loading @@ -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); } Loading @@ -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) { Loading @@ -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) { Loading @@ -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); } } } /** Loading
packages/SystemUI/src/com/android/systemui/statusbar/SmartReplyController.java +19 −7 Original line number Diff line number Diff line Loading @@ -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); Loading Loading @@ -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); } }