New Pipeline: Remote Input 1/4: Extract legacy pipeline logic within NotificationRemoteInputManager am: d4e9cfb087

Original change: https://googleplex-android-review.googlesource.com/c/platform/frameworks/base/+/16167379

Change-Id: I7386cd8dafb9f609b69796a2d1efc9dd7e106e6b
This commit is contained in:
Jeff DeCew
2021-11-02 15:27:15 +00:00
committed by Automerger Merge Worker
5 changed files with 373 additions and 230 deletions

View File

@@ -15,8 +15,6 @@
*/ */
package com.android.systemui.statusbar; package com.android.systemui.statusbar;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager; import android.app.ActivityManager;
import android.app.ActivityOptions; import android.app.ActivityOptions;
import android.app.KeyguardManager; import android.app.KeyguardManager;
@@ -48,6 +46,9 @@ import android.widget.RemoteViews;
import android.widget.RemoteViews.InteractionHandler; import android.widget.RemoteViews.InteractionHandler;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.statusbar.NotificationVisibility; import com.android.internal.statusbar.NotificationVisibility;
@@ -55,10 +56,12 @@ import com.android.systemui.Dumpable;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.dagger.StatusBarDependenciesModule; import com.android.systemui.statusbar.dagger.StatusBarDependenciesModule;
import com.android.systemui.statusbar.notification.NotificationEntryListener; import com.android.systemui.statusbar.notification.NotificationEntryListener;
import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo; import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
import com.android.systemui.statusbar.notification.logging.NotificationLogger; import com.android.systemui.statusbar.notification.logging.NotificationLogger;
@@ -93,27 +96,7 @@ public class NotificationRemoteInputManager implements Dumpable {
private static final boolean DEBUG = false; private static final boolean DEBUG = false;
private static final String TAG = "NotifRemoteInputManager"; private static final String TAG = "NotifRemoteInputManager";
/** private RemoteInputListener mRemoteInputListener;
* How long to wait before auto-dismissing a notification that was kept for remote input, and
* has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel
* these given that they technically don't exist anymore. We wait a bit in case the app issues
* an update.
*/
private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
/**
* 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<NotificationEntry> mEntriesKeptForRemoteInputActive =
new ArraySet<>();
// Dependencies: // Dependencies:
private final NotificationLockscreenUserManager mLockscreenUserManager; private final NotificationLockscreenUserManager mLockscreenUserManager;
@@ -125,6 +108,7 @@ public class NotificationRemoteInputManager implements Dumpable {
private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy; private final Lazy<Optional<StatusBar>> mStatusBarOptionalLazy;
protected final Context mContext; protected final Context mContext;
protected final FeatureFlags mFeatureFlags;
private final UserManager mUserManager; private final UserManager mUserManager;
private final KeyguardManager mKeyguardManager; private final KeyguardManager mKeyguardManager;
private final StatusBarStateController mStatusBarStateController; private final StatusBarStateController mStatusBarStateController;
@@ -132,11 +116,8 @@ public class NotificationRemoteInputManager implements Dumpable {
private final NotificationClickNotifier mClickNotifier; private final NotificationClickNotifier mClickNotifier;
protected RemoteInputController mRemoteInputController; protected RemoteInputController mRemoteInputController;
protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
mNotificationLifetimeFinishedCallback;
protected IStatusBarService mBarService; protected IStatusBarService mBarService;
protected Callback mCallback; protected Callback mCallback;
protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
private final List<RemoteInputController.Callback> mControllerCallbacks = new ArrayList<>(); private final List<RemoteInputController.Callback> mControllerCallbacks = new ArrayList<>();
@@ -226,6 +207,7 @@ public class NotificationRemoteInputManager implements Dumpable {
ViewGroup actionGroup = (ViewGroup) parent; ViewGroup actionGroup = (ViewGroup) parent;
buttonIndex = actionGroup.indexOfChild(view); buttonIndex = actionGroup.indexOfChild(view);
} }
// FIXME: get this for the new pipeline!
final int count = mEntryManager.getActiveNotificationsCount(); final int count = mEntryManager.getActiveNotificationsCount();
final int rank = entry.getRanking().getRank(); final int rank = entry.getRanking().getRank();
@@ -283,9 +265,11 @@ public class NotificationRemoteInputManager implements Dumpable {
*/ */
public NotificationRemoteInputManager( public NotificationRemoteInputManager(
Context context, Context context,
FeatureFlags featureFlags,
NotificationLockscreenUserManager lockscreenUserManager, NotificationLockscreenUserManager lockscreenUserManager,
SmartReplyController smartReplyController, SmartReplyController smartReplyController,
NotificationEntryManager notificationEntryManager, NotificationEntryManager notificationEntryManager,
NotifPipeline notifPipeline,
Lazy<Optional<StatusBar>> statusBarOptionalLazy, Lazy<Optional<StatusBar>> statusBarOptionalLazy,
StatusBarStateController statusBarStateController, StatusBarStateController statusBarStateController,
@Main Handler mainHandler, @Main Handler mainHandler,
@@ -294,6 +278,7 @@ public class NotificationRemoteInputManager implements Dumpable {
ActionClickLogger logger, ActionClickLogger logger,
DumpManager dumpManager) { DumpManager dumpManager) {
mContext = context; mContext = context;
mFeatureFlags = featureFlags;
mLockscreenUserManager = lockscreenUserManager; mLockscreenUserManager = lockscreenUserManager;
mSmartReplyController = smartReplyController; mSmartReplyController = smartReplyController;
mEntryManager = notificationEntryManager; mEntryManager = notificationEntryManager;
@@ -303,7 +288,10 @@ public class NotificationRemoteInputManager implements Dumpable {
mBarService = IStatusBarService.Stub.asInterface( mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE)); ServiceManager.getService(Context.STATUS_BAR_SERVICE));
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
addLifetimeExtenders(); if (!featureFlags.isNewNotifPipelineRenderingEnabled()) {
mRemoteInputListener = createLegacyRemoteInputLifetimeExtender(mainHandler,
notificationEntryManager, smartReplyController);
}
mKeyguardManager = context.getSystemService(KeyguardManager.class); mKeyguardManager = context.getSystemService(KeyguardManager.class);
mStatusBarStateController = statusBarStateController; mStatusBarStateController = statusBarStateController;
mRemoteInputUriController = remoteInputUriController; mRemoteInputUriController = remoteInputUriController;
@@ -335,10 +323,20 @@ public class NotificationRemoteInputManager implements Dumpable {
}); });
} }
@NonNull
@VisibleForTesting
protected LegacyRemoteInputLifetimeExtender createLegacyRemoteInputLifetimeExtender(
Handler mainHandler,
NotificationEntryManager notificationEntryManager,
SmartReplyController smartReplyController) {
return new LegacyRemoteInputLifetimeExtender();
}
/** Initializes this component with the provided dependencies. */ /** Initializes this component with the provided dependencies. */
public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) { public void setUpWithCallback(Callback callback, RemoteInputController.Delegate delegate) {
mCallback = callback; mCallback = callback;
mRemoteInputController = new RemoteInputController(delegate, mRemoteInputUriController); mRemoteInputController = new RemoteInputController(delegate, mRemoteInputUriController);
mRemoteInputListener.setRemoteInputController(mRemoteInputController);
// Register all stored callbacks from before the Controller was initialized. // Register all stored callbacks from before the Controller was initialized.
for (RemoteInputController.Callback cb : mControllerCallbacks) { for (RemoteInputController.Callback cb : mControllerCallbacks) {
mRemoteInputController.addCallback(cb); mRemoteInputController.addCallback(cb);
@@ -347,19 +345,8 @@ public class NotificationRemoteInputManager implements Dumpable {
mRemoteInputController.addCallback(new RemoteInputController.Callback() { mRemoteInputController.addCallback(new RemoteInputController.Callback() {
@Override @Override
public void onRemoteInputSent(NotificationEntry entry) { public void onRemoteInputSent(NotificationEntry entry) {
if (FORCE_REMOTE_INPUT_HISTORY if (mRemoteInputListener != null) {
&& isNotificationKeptForRemoteInputHistory(entry.getKey())) { mRemoteInputListener.onRemoteInputSent(entry);
mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
} 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.
mMainHandler.postDelayed(() -> {
if (mEntriesKeptForRemoteInputActive.remove(entry)) {
mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
}
}, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
} }
try { try {
mBarService.onNotificationDirectReplied(entry.getSbn().getKey()); mBarService.onNotificationDirectReplied(entry.getSbn().getKey());
@@ -381,12 +368,13 @@ public class NotificationRemoteInputManager implements Dumpable {
} }
} }
}); });
mSmartReplyController.setCallback((entry, reply) -> { if (!mFeatureFlags.isNewNotifPipelineRenderingEnabled()) {
StatusBarNotification newSbn = // FIXME: Don't forget to implement this in the coordinator!
rebuildNotificationWithRemoteInputInserted(entry, reply, true /* showSpinner */, mSmartReplyController.setCallback((entry, reply) -> {
null /* mimeType */, null /* uri */); StatusBarNotification newSbn = rebuildNotificationForSendingSmartReply(entry, reply);
mEntryManager.updateNotification(newSbn, null /* ranking */); mEntryManager.updateNotification(newSbn, null /* ranking */);
}); });
}
} }
public void addControllerCallback(RemoteInputController.Callback callback) { public void addControllerCallback(RemoteInputController.Callback callback) {
@@ -574,51 +562,39 @@ public class NotificationRemoteInputManager implements Dumpable {
if (v == null) { if (v == null) {
return null; return null;
} }
return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG); return v.findViewWithTag(RemoteInputView.VIEW_TAG);
}
/**
* 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 ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() { public ArrayList<NotificationLifetimeExtender> getLifetimeExtenders() {
return mLifetimeExtenders; // OLD pipeline code ONLY; can assume implementation
return ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener).mLifetimeExtenders;
} }
@VisibleForTesting @VisibleForTesting
void onPerformRemoveNotification(NotificationEntry entry, final String key) { void onPerformRemoveNotification(NotificationEntry entry, final String key) {
if (mKeysKeptForRemoteInputHistory.contains(key)) { // OLD pipeline code ONLY; can assume implementation
mKeysKeptForRemoteInputHistory.remove(key); ((LegacyRemoteInputLifetimeExtender) mRemoteInputListener)
} .mKeysKeptForRemoteInputHistory.remove(key);
if (isRemoteInputActive(entry)) { if (isRemoteInputActive(entry)) {
entry.mRemoteEditImeVisible = false; entry.mRemoteEditImeVisible = false;
mRemoteInputController.removeRemoteInput(entry, null); mRemoteInputController.removeRemoteInput(entry, null);
} }
} }
/** Informs the remote input system that the panel has collapsed */
public void onPanelCollapsed() { public void onPanelCollapsed() {
for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) { if (mRemoteInputListener != null) {
NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i); mRemoteInputListener.onPanelCollapsed();
if (mRemoteInputController != null) {
mRemoteInputController.removeRemoteInput(entry, null);
}
if (mNotificationLifetimeFinishedCallback != null) {
mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
}
} }
mEntriesKeptForRemoteInputActive.clear();
} }
/** Returns whether the given notification is lifetime extended because of remote input */
public boolean isNotificationKeptForRemoteInputHistory(String key) { public boolean isNotificationKeptForRemoteInputHistory(String key) {
return mKeysKeptForRemoteInputHistory.contains(key); return mRemoteInputListener != null
&& mRemoteInputListener.isNotificationKeptForRemoteInputHistory(key);
} }
/** Returns whether the notification should be lifetime extended for remote input history */
public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) { public boolean shouldKeepForRemoteInputHistory(NotificationEntry entry) {
if (!FORCE_REMOTE_INPUT_HISTORY) { if (!FORCE_REMOTE_INPUT_HISTORY) {
return false; return false;
@@ -636,16 +612,12 @@ public class NotificationRemoteInputManager implements Dumpable {
if (entry == null) { if (entry == null) {
return; return;
} }
final String key = entry.getKey(); if (mRemoteInputListener != null) {
if (isNotificationKeptForRemoteInputHistory(key)) { mRemoteInputListener.releaseNotificationIfKeptForRemoteInputHistory(entry);
mMainHandler.postDelayed(() -> {
if (isNotificationKeptForRemoteInputHistory(key)) {
mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
}
}, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
} }
} }
/** Returns whether the notification should be lifetime extended for smart reply history */
public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) { public boolean shouldKeepForSmartReplyHistory(NotificationEntry entry) {
if (!FORCE_REMOTE_INPUT_HISTORY) { if (!FORCE_REMOTE_INPUT_HISTORY) {
return false; return false;
@@ -661,13 +633,36 @@ public class NotificationRemoteInputManager implements Dumpable {
} }
} }
@VisibleForTesting // FIXME: Move to a helper class and test separately
StatusBarNotification rebuildNotificationForCanceledSmartReplies( public StatusBarNotification rebuildNotificationForSendingSmartReply(NotificationEntry entry,
CharSequence reply) {
return rebuildNotificationWithRemoteInputInserted(entry, reply,
true /* showSpinner */,
null /* mimeType */, null /* uri */);
}
// FIXME: Move to a helper class and test separately
public StatusBarNotification rebuildNotificationForCanceledSmartReplies(
NotificationEntry entry) { NotificationEntry entry) {
return rebuildNotificationWithRemoteInputInserted(entry, null /* remoteInputTest */, return rebuildNotificationWithRemoteInputInserted(entry, null /* remoteInputTest */,
false /* showSpinner */, null /* mimeType */, null /* uri */); false /* showSpinner */, null /* mimeType */, null /* uri */);
} }
// FIXME: Move to a helper class and test separately
public StatusBarNotification rebuildNotificationForBasicExtension(NotificationEntry entry) {
CharSequence remoteInputText = entry.remoteInputText;
if (TextUtils.isEmpty(remoteInputText)) {
remoteInputText = entry.remoteInputTextWhenReset;
}
String remoteInputMimeType = entry.remoteInputMimeType;
Uri remoteInputUri = entry.remoteInputUri;
StatusBarNotification newSbn = rebuildNotificationWithRemoteInputInserted(entry,
remoteInputText, false /* showSpinner */, remoteInputMimeType,
remoteInputUri);
return newSbn;
}
// FIXME: Move to a helper class and test separately
@VisibleForTesting @VisibleForTesting
StatusBarNotification rebuildNotificationWithRemoteInputInserted(NotificationEntry entry, StatusBarNotification rebuildNotificationWithRemoteInputInserted(NotificationEntry entry,
CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) { CharSequence remoteInputText, boolean showSpinner, String mimeType, Uri uri) {
@@ -714,11 +709,9 @@ public class NotificationRemoteInputManager implements Dumpable {
@Override @Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("NotificationRemoteInputManager state:"); if (mRemoteInputListener instanceof Dumpable) {
pw.print(" mKeysKeptForRemoteInputHistory: "); ((Dumpable) mRemoteInputListener).dump(fd, pw, args);
pw.println(mKeysKeptForRemoteInputHistory); }
pw.print(" mEntriesKeptForRemoteInputActive: ");
pw.println(mEntriesKeptForRemoteInputActive);
} }
public void bindRow(ExpandableNotificationRow row) { public void bindRow(ExpandableNotificationRow row) {
@@ -734,11 +727,6 @@ public class NotificationRemoteInputManager implements Dumpable {
return mInteractionHandler; return mInteractionHandler;
} }
@VisibleForTesting
public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
return mEntriesKeptForRemoteInputActive;
}
public boolean isRemoteInputActive() { public boolean isRemoteInputActive() {
return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive(); return mRemoteInputController != null && mRemoteInputController.isRemoteInputActive();
} }
@@ -757,131 +745,6 @@ public class NotificationRemoteInputManager implements Dumpable {
} }
} }
/**
* 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 NotificationEntry entry) {
return shouldKeepForRemoteInputHistory(entry);
}
@Override
public void setShouldManageLifetime(NotificationEntry entry,
boolean shouldExtend) {
if (shouldExtend) {
CharSequence remoteInputText = entry.remoteInputText;
if (TextUtils.isEmpty(remoteInputText)) {
remoteInputText = entry.remoteInputTextWhenReset;
}
String remoteInputMimeType = entry.remoteInputMimeType;
Uri remoteInputUri = entry.remoteInputUri;
StatusBarNotification newSbn = rebuildNotificationWithRemoteInputInserted(entry,
remoteInputText, false /* showSpinner */, remoteInputMimeType,
remoteInputUri);
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.isRemoved()) {
return;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Keeping notification around after sending remote input "
+ entry.getKey());
}
mKeysKeptForRemoteInputHistory.add(entry.getKey());
} else {
mKeysKeptForRemoteInputHistory.remove(entry.getKey());
}
}
}
/**
* 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 NotificationEntry entry) {
return shouldKeepForSmartReplyHistory(entry);
}
@Override
public void setShouldManageLifetime(NotificationEntry entry,
boolean shouldExtend) {
if (shouldExtend) {
StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
if (newSbn == null) {
return;
}
mEntryManager.updateNotification(newSbn, null);
if (entry.isRemoved()) {
return;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Keeping notification around after sending smart reply "
+ entry.getKey());
}
mKeysKeptForRemoteInputHistory.add(entry.getKey());
} else {
mKeysKeptForRemoteInputHistory.remove(entry.getKey());
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 NotificationEntry entry) {
return isRemoteInputActive(entry);
}
@Override
public void setShouldManageLifetime(NotificationEntry entry,
boolean shouldExtend) {
if (shouldExtend) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Keeping notification around while remote input active "
+ entry.getKey());
}
mEntriesKeptForRemoteInputActive.add(entry);
} else {
mEntriesKeptForRemoteInputActive.remove(entry);
}
}
}
/** /**
* Callback for various remote input related events, or for providing information that * Callback for various remote input related events, or for providing information that
* NotificationRemoteInputManager needs to know to decide what to do. * NotificationRemoteInputManager needs to know to decide what to do.
@@ -975,4 +838,250 @@ public class NotificationRemoteInputManager implements Dumpable {
*/ */
boolean showBouncerIfNecessary(); boolean showBouncerIfNecessary();
} }
public interface RemoteInputListener {
void onRemoteInputSent(NotificationEntry entry);
void onPanelCollapsed();
boolean isNotificationKeptForRemoteInputHistory(String key);
void releaseNotificationIfKeptForRemoteInputHistory(@NonNull NotificationEntry entry);
void setRemoteInputController(@NonNull RemoteInputController remoteInputController);
}
@VisibleForTesting
protected class LegacyRemoteInputLifetimeExtender implements RemoteInputListener, Dumpable {
/**
* How long to wait before auto-dismissing a notification that was kept for remote input,
* and has now sent a remote input. We auto-dismiss, because the app may not see a reason to
* cancel these given that they technically don't exist anymore. We wait a bit in case the
* app issues an update.
*/
private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200;
/**
* 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<NotificationEntry> mEntriesKeptForRemoteInputActive =
new ArraySet<>();
protected NotificationLifetimeExtender.NotificationSafeToRemoveCallback
mNotificationLifetimeFinishedCallback;
protected final ArrayList<NotificationLifetimeExtender> mLifetimeExtenders =
new ArrayList<>();
private RemoteInputController mRemoteInputController;
LegacyRemoteInputLifetimeExtender() {
addLifetimeExtenders();
}
/**
* 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());
}
@Override
public void setRemoteInputController(@NonNull RemoteInputController remoteInputController) {
mRemoteInputController= remoteInputController;
}
@Override
public void onRemoteInputSent(NotificationEntry entry) {
if (FORCE_REMOTE_INPUT_HISTORY
&& isNotificationKeptForRemoteInputHistory(entry.getKey())) {
mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
} 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.
mMainHandler.postDelayed(() -> {
if (mEntriesKeptForRemoteInputActive.remove(entry)) {
mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
}
}, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
}
}
@Override
public void onPanelCollapsed() {
for (int i = 0; i < mEntriesKeptForRemoteInputActive.size(); i++) {
NotificationEntry entry = mEntriesKeptForRemoteInputActive.valueAt(i);
if (mRemoteInputController != null) {
mRemoteInputController.removeRemoteInput(entry, null);
}
if (mNotificationLifetimeFinishedCallback != null) {
mNotificationLifetimeFinishedCallback.onSafeToRemove(entry.getKey());
}
}
mEntriesKeptForRemoteInputActive.clear();
}
@Override
public boolean isNotificationKeptForRemoteInputHistory(String key) {
return mKeysKeptForRemoteInputHistory.contains(key);
}
@Override
public void releaseNotificationIfKeptForRemoteInputHistory(
@NonNull NotificationEntry entry) {
final String key = entry.getKey();
if (isNotificationKeptForRemoteInputHistory(key)) {
mMainHandler.postDelayed(() -> {
if (isNotificationKeptForRemoteInputHistory(key)) {
mNotificationLifetimeFinishedCallback.onSafeToRemove(key);
}
}, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY);
}
}
@VisibleForTesting
public Set<NotificationEntry> getEntriesKeptForRemoteInputActive() {
return mEntriesKeptForRemoteInputActive;
}
@Override
public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
@NonNull String[] args) {
pw.println("LegacyRemoteInputLifetimeExtender:");
pw.print(" mKeysKeptForRemoteInputHistory: ");
pw.println(mKeysKeptForRemoteInputHistory);
pw.print(" mEntriesKeptForRemoteInputActive: ");
pw.println(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 NotificationEntry entry) {
return shouldKeepForRemoteInputHistory(entry);
}
@Override
public void setShouldManageLifetime(NotificationEntry entry,
boolean shouldExtend) {
if (shouldExtend) {
StatusBarNotification newSbn = rebuildNotificationForBasicExtension(entry);
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.isRemoved()) {
return;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Keeping notification around after sending remote input "
+ entry.getKey());
}
mKeysKeptForRemoteInputHistory.add(entry.getKey());
} else {
mKeysKeptForRemoteInputHistory.remove(entry.getKey());
}
}
}
/**
* 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 NotificationEntry entry) {
return shouldKeepForSmartReplyHistory(entry);
}
@Override
public void setShouldManageLifetime(NotificationEntry entry,
boolean shouldExtend) {
if (shouldExtend) {
StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
if (newSbn == null) {
return;
}
mEntryManager.updateNotification(newSbn, null);
if (entry.isRemoved()) {
return;
}
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Keeping notification around after sending smart reply "
+ entry.getKey());
}
mKeysKeptForRemoteInputHistory.add(entry.getKey());
} else {
mKeysKeptForRemoteInputHistory.remove(entry.getKey());
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 NotificationEntry entry) {
return isRemoteInputActive(entry);
}
@Override
public void setShouldManageLifetime(NotificationEntry entry,
boolean shouldExtend) {
if (shouldExtend) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Keeping notification around while remote input active "
+ entry.getKey());
}
mEntriesKeptForRemoteInputActive.add(entry);
} else {
mEntriesKeptForRemoteInputActive.remove(entry);
}
}
}
}
} }

View File

@@ -299,6 +299,9 @@ public class RemoteInputController {
default void onRemoteInputSent(NotificationEntry entry) {} default void onRemoteInputSent(NotificationEntry entry) {}
} }
/**
* This is a delegate which implements some view controller pieces of the remote input process
*/
public interface Delegate { public interface Delegate {
/** /**
* Activate remote input if necessary. * Activate remote input if necessary.

View File

@@ -96,9 +96,11 @@ public interface StatusBarDependenciesModule {
@Provides @Provides
static NotificationRemoteInputManager provideNotificationRemoteInputManager( static NotificationRemoteInputManager provideNotificationRemoteInputManager(
Context context, Context context,
FeatureFlags featureFlags,
NotificationLockscreenUserManager lockscreenUserManager, NotificationLockscreenUserManager lockscreenUserManager,
SmartReplyController smartReplyController, SmartReplyController smartReplyController,
NotificationEntryManager notificationEntryManager, NotificationEntryManager notificationEntryManager,
NotifPipeline notifPipeline,
Lazy<Optional<StatusBar>> statusBarOptionalLazy, Lazy<Optional<StatusBar>> statusBarOptionalLazy,
StatusBarStateController statusBarStateController, StatusBarStateController statusBarStateController,
Handler mainHandler, Handler mainHandler,
@@ -108,9 +110,11 @@ public interface StatusBarDependenciesModule {
DumpManager dumpManager) { DumpManager dumpManager) {
return new NotificationRemoteInputManager( return new NotificationRemoteInputManager(
context, context,
featureFlags,
lockscreenUserManager, lockscreenUserManager,
smartReplyController, smartReplyController,
notificationEntryManager, notificationEntryManager,
notifPipeline,
statusBarOptionalLazy, statusBarOptionalLazy,
statusBarStateController, statusBarStateController,
mainHandler, mainHandler,

View File

@@ -22,14 +22,16 @@ import android.service.notification.StatusBarNotification;
import android.testing.AndroidTestingRunner; import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper; import android.testing.TestableLooper;
import androidx.annotation.NonNull;
import androidx.test.filters.SmallTest; import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputActiveExtender; import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.RemoteInputActiveExtender;
import com.android.systemui.statusbar.NotificationRemoteInputManager.RemoteInputHistoryExtender; import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.RemoteInputHistoryExtender;
import com.android.systemui.statusbar.NotificationRemoteInputManager.SmartReplyHistoryExtender; import com.android.systemui.statusbar.NotificationRemoteInputManager.LegacyRemoteInputLifetimeExtender.SmartReplyHistoryExtender;
import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
@@ -76,12 +78,15 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
private RemoteInputHistoryExtender mRemoteInputHistoryExtender; private RemoteInputHistoryExtender mRemoteInputHistoryExtender;
private SmartReplyHistoryExtender mSmartReplyHistoryExtender; private SmartReplyHistoryExtender mSmartReplyHistoryExtender;
private RemoteInputActiveExtender mRemoteInputActiveExtender; private RemoteInputActiveExtender mRemoteInputActiveExtender;
private TestableNotificationRemoteInputManager.FakeLegacyRemoteInputLifetimeExtender
mLegacyRemoteInputLifetimeExtender;
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext, mRemoteInputManager = new TestableNotificationRemoteInputManager(mContext,
mock(FeatureFlags.class),
mLockscreenUserManager, mSmartReplyController, mEntryManager, mLockscreenUserManager, mSmartReplyController, mEntryManager,
() -> Optional.of(mock(StatusBar.class)), () -> Optional.of(mock(StatusBar.class)),
mStateController, mStateController,
@@ -151,18 +156,19 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
public void testNotificationWithRemoteInputActiveIsRemovedOnCollapse() { public void testNotificationWithRemoteInputActiveIsRemovedOnCollapse() {
mRemoteInputActiveExtender.setShouldManageLifetime(mEntry, true /* shouldManage */); mRemoteInputActiveExtender.setShouldManageLifetime(mEntry, true /* shouldManage */);
assertEquals(mRemoteInputManager.getEntriesKeptForRemoteInputActive(), assertEquals(mLegacyRemoteInputLifetimeExtender.getEntriesKeptForRemoteInputActive(),
Sets.newArraySet(mEntry)); Sets.newArraySet(mEntry));
mRemoteInputManager.onPanelCollapsed(); mRemoteInputManager.onPanelCollapsed();
assertTrue(mRemoteInputManager.getEntriesKeptForRemoteInputActive().isEmpty()); assertTrue(
mLegacyRemoteInputLifetimeExtender.getEntriesKeptForRemoteInputActive().isEmpty());
} }
@Test @Test
public void testRebuildWithRemoteInput_noExistingInput_image() { public void testRebuildWithRemoteInput_noExistingInput_image() {
Uri uri = mock(Uri.class); Uri uri = mock(Uri.class);
String mimeType = "image/jpeg"; String mimeType = "image/jpeg";
String text = "image inserted"; String text = "image inserted";
StatusBarNotification newSbn = StatusBarNotification newSbn =
mRemoteInputManager.rebuildNotificationWithRemoteInputInserted( mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
@@ -229,7 +235,7 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
public void testRebuildWithRemoteInput_withExistingInput_image() { public void testRebuildWithRemoteInput_withExistingInput_image() {
// Setup a notification entry with 1 remote input. // Setup a notification entry with 1 remote input.
Uri uri = mock(Uri.class); Uri uri = mock(Uri.class);
String mimeType = "image/jpeg"; String mimeType = "image/jpeg";
String text = "image inserted"; String text = "image inserted";
StatusBarNotification newSbn = StatusBarNotification newSbn =
mRemoteInputManager.rebuildNotificationWithRemoteInputInserted( mRemoteInputManager.rebuildNotificationWithRemoteInputInserted(
@@ -266,6 +272,7 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
TestableNotificationRemoteInputManager( TestableNotificationRemoteInputManager(
Context context, Context context,
FeatureFlags featureFlags,
NotificationLockscreenUserManager lockscreenUserManager, NotificationLockscreenUserManager lockscreenUserManager,
SmartReplyController smartReplyController, SmartReplyController smartReplyController,
NotificationEntryManager notificationEntryManager, NotificationEntryManager notificationEntryManager,
@@ -278,6 +285,7 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
DumpManager dumpManager) { DumpManager dumpManager) {
super( super(
context, context,
featureFlags,
lockscreenUserManager, lockscreenUserManager,
smartReplyController, smartReplyController,
notificationEntryManager, notificationEntryManager,
@@ -297,14 +305,28 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
mRemoteInputController = controller; mRemoteInputController = controller;
} }
@NonNull
@Override @Override
protected void addLifetimeExtenders() { protected LegacyRemoteInputLifetimeExtender createLegacyRemoteInputLifetimeExtender(
mRemoteInputActiveExtender = new RemoteInputActiveExtender(); Handler mainHandler,
mRemoteInputHistoryExtender = new RemoteInputHistoryExtender(); NotificationEntryManager notificationEntryManager,
mSmartReplyHistoryExtender = new SmartReplyHistoryExtender(); SmartReplyController smartReplyController) {
mLifetimeExtenders.add(mRemoteInputHistoryExtender); mLegacyRemoteInputLifetimeExtender = new FakeLegacyRemoteInputLifetimeExtender();
mLifetimeExtenders.add(mSmartReplyHistoryExtender); return mLegacyRemoteInputLifetimeExtender;
mLifetimeExtenders.add(mRemoteInputActiveExtender);
} }
class FakeLegacyRemoteInputLifetimeExtender extends LegacyRemoteInputLifetimeExtender {
@Override
protected void addLifetimeExtenders() {
mRemoteInputActiveExtender = new RemoteInputActiveExtender();
mRemoteInputHistoryExtender = new RemoteInputHistoryExtender();
mSmartReplyHistoryExtender = new SmartReplyHistoryExtender();
mLifetimeExtenders.add(mRemoteInputHistoryExtender);
mLifetimeExtenders.add(mSmartReplyHistoryExtender);
mLifetimeExtenders.add(mRemoteInputActiveExtender);
}
}
} }
} }

View File

@@ -39,8 +39,10 @@ import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.R; import com.android.systemui.R;
import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestCase;
import com.android.systemui.dump.DumpManager; import com.android.systemui.dump.DumpManager;
import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.plugins.statusbar.StatusBarStateController;
import com.android.systemui.statusbar.notification.NotificationEntryManager; import com.android.systemui.statusbar.notification.NotificationEntryManager;
import com.android.systemui.statusbar.notification.collection.NotifPipeline;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBar;
@@ -95,8 +97,11 @@ public class SmartReplyControllerTest extends SysuiTestCase {
mSmartReplyController); mSmartReplyController);
mRemoteInputManager = new NotificationRemoteInputManager(mContext, mRemoteInputManager = new NotificationRemoteInputManager(mContext,
mock(FeatureFlags.class),
mock(NotificationLockscreenUserManager.class), mSmartReplyController, mock(NotificationLockscreenUserManager.class), mSmartReplyController,
mNotificationEntryManager, () -> Optional.of(mock(StatusBar.class)), mNotificationEntryManager,
mock(NotifPipeline.class),
() -> Optional.of(mock(StatusBar.class)),
mStatusBarStateController, mStatusBarStateController,
Handler.createAsync(Looper.myLooper()), Handler.createAsync(Looper.myLooper()),
mRemoteInputUriController, mRemoteInputUriController,