Merge "Keep notification when sending smart reply." into pi-dev

This commit is contained in:
Selim Cinek
2018-05-18 16:20:40 +00:00
committed by Android (Google) Code Review
12 changed files with 190 additions and 45 deletions

View File

@@ -983,6 +983,17 @@ public class Notification implements Parcelable
*/
public static final String EXTRA_SHOW_REMOTE_INPUT_SPINNER = "android.remoteInputSpinner";
/**
* {@link #extras} key: boolean as supplied to
* {@link Builder#setHideSmartReplies(boolean)}.
*
* If set to true, then any smart reply buttons will be hidden.
*
* @see Builder#setHideSmartReplies(boolean)
* @hide
*/
public static final String EXTRA_HIDE_SMART_REPLIES = "android.hideSmartReplies";
/**
* {@link #extras} key: this is a small piece of additional text as supplied to
* {@link Builder#setContentInfo(CharSequence)}.
@@ -3594,6 +3605,15 @@ public class Notification implements Parcelable
return this;
}
/**
* Sets whether smart reply buttons should be hidden.
* @hide
*/
public Builder setHideSmartReplies(boolean hideSmartReplies) {
mN.extras.putBoolean(EXTRA_HIDE_SMART_REPLIES, hideSmartReplies);
return this;
}
/**
* Sets the number of items this notification represents. May be displayed as a badge count
* for Launchers that support badging.

View File

@@ -1384,6 +1384,13 @@ public class NotificationContentView extends FrameLayout {
smartReplyContainer.setVisibility(View.GONE);
return null;
}
// If we are keeping the notification around while sending we don't want to add the buttons.
boolean hideSmartReplies = entry.notification.getNotification()
.extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false);
if (hideSmartReplies) {
smartReplyContainer.setVisibility(View.GONE);
return null;
}
SmartReplyView smartReplyView = null;
if (smartReplyContainer.getChildCount() == 0) {
smartReplyView = SmartReplyView.inflate(mContext, smartReplyContainer);

View File

@@ -113,6 +113,8 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
Dependency.get(ForegroundServiceController.class);
protected final NotificationListener mNotificationListener =
Dependency.get(NotificationListener.class);
private final SmartReplyController mSmartReplyController =
Dependency.get(SmartReplyController.class);
protected IStatusBarService mBarService;
protected NotificationPresenter mPresenter;
@@ -127,6 +129,13 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
protected boolean mDisableNotificationAlerts;
protected NotificationListContainer mListContainer;
private ExpandableNotificationRow.OnAppOpsClickListener mOnAppOpsClickListener;
/**
* Notifications with keys in this set are not actually around anymore. We kept them around
* when they were canceled in response to a remote input interaction. This allows us to show
* what you replied and allows you to continue typing into it.
*/
private final ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>();
private final class NotificationClicker implements View.OnClickListener {
@@ -220,6 +229,8 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
}
pw.print(" mUseHeadsUp=");
pw.println(mUseHeadsUp);
pw.print(" mKeysKeptForRemoteInput: ");
pw.println(mKeysKeptForRemoteInput);
}
public NotificationEntryManager(Context context) {
@@ -374,6 +385,12 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
final NotificationVisibility nv = NotificationVisibility.obtain(n.getKey(), rank, count,
true);
NotificationData.Entry entry = mNotificationData.get(n.getKey());
if (FORCE_REMOTE_INPUT_HISTORY
&& mKeysKeptForRemoteInput.contains(n.getKey())) {
mKeysKeptForRemoteInput.remove(n.getKey());
}
mRemoteInputManager.onPerformRemoveNotification(n, entry);
final String pkg = n.getPackageName();
final String tag = n.getTag();
@@ -491,10 +508,35 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
}
if (updated) {
Log.w(TAG, "Keeping notification around after sending remote input "+ entry.key);
mRemoteInputManager.getKeysKeptForRemoteInput().add(entry.key);
addKeyKeptForRemoteInput(entry.key);
return;
}
}
if (FORCE_REMOTE_INPUT_HISTORY
&& shouldKeepForSmartReply(entry)
&& entry.row != null && !entry.row.isDismissed()) {
// Turn off the spinner and hide buttons when an app cancels the notification.
StatusBarNotification newSbn = rebuildNotificationForCanceledSmartReplies(entry);
boolean updated = false;
try {
updateNotificationInternal(newSbn, null);
updated = true;
} catch (InflationException e) {
// Ignore just don't keep the notification around.
}
// Treat the reply as longer sending.
mSmartReplyController.stopSending(entry);
if (updated) {
Log.w(TAG, "Keeping notification around after sending smart reply " + entry.key);
addKeyKeptForRemoteInput(entry.key);
return;
}
}
// Actually removing notification so smart reply controller can forget about it.
mSmartReplyController.stopSending(entry);
if (deferRemoval) {
mLatestRankingMap = ranking;
mHeadsUpEntriesToRemoveOnSwitch.add(mHeadsUpManager.getEntry(key));
@@ -536,18 +578,21 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
Notification.Builder b = Notification.Builder
.recoverBuilder(mContext, sbn.getNotification().clone());
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);
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);
}
newHistory[0] = String.valueOf(remoteInputText);
b.setRemoteInputHistory(newHistory);
b.setShowRemoteInputSpinner(showSpinner);
b.setHideSmartReplies(true);
Notification newNotification = b.build();
@@ -563,6 +608,17 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
return newSbn;
}
@VisibleForTesting
StatusBarNotification rebuildNotificationForCanceledSmartReplies(
NotificationData.Entry entry) {
return rebuildNotificationWithRemoteInput(entry, null /* remoteInputTest */,
false /* showSpinner */);
}
private boolean shouldKeepForSmartReply(NotificationData.Entry entry) {
return entry != null && mSmartReplyController.isSendingSmartReply(entry.key);
}
private boolean shouldKeepForRemoteInput(NotificationData.Entry entry) {
if (entry == null) {
return false;
@@ -792,6 +848,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
}
mHeadsUpEntriesToRemoveOnSwitch.remove(entry);
mRemoteInputManager.onUpdateNotification(entry);
mSmartReplyController.stopSending(entry);
if (key.equals(mGutsManager.getKeyToRemoveOnGutsClosed())) {
mGutsManager.setKeyToRemoveOnGutsClosed(null);
@@ -955,6 +1012,20 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
return mHeadsUpManager.isHeadsUp(key);
}
public boolean isNotificationKeptForRemoteInput(String key) {
return mKeysKeptForRemoteInput.contains(key);
}
public void removeKeyKeptForRemoteInput(String key) {
mKeysKeptForRemoteInput.remove(key);
}
public void addKeyKeptForRemoteInput(String key) {
if (FORCE_REMOTE_INPUT_HISTORY) {
mKeysKeptForRemoteInput.add(key);
}
}
/**
* Callback for NotificationEntryManager.
*/

View File

@@ -75,7 +75,7 @@ public class NotificationListener extends NotificationListenerWithPlugins {
mPresenter.getHandler().post(() -> {
processForRemoteInput(sbn.getNotification(), mContext);
String key = sbn.getKey();
mRemoteInputManager.getKeysKeptForRemoteInput().remove(key);
mEntryManager.removeKeyKeptForRemoteInput(key);
boolean isUpdate =
mEntryManager.getNotificationData().get(key) != null;
// In case we don't allow child notifications, we ignore children of

View File

@@ -77,12 +77,6 @@ public class NotificationRemoteInputManager implements Dumpable {
protected final NotificationLockscreenUserManager mLockscreenUserManager =
Dependency.get(NotificationLockscreenUserManager.class);
/**
* Notifications with keys in this set are not actually around anymore. We kept them around
* when they were canceled in response to a remote input interaction. This allows us to show
* what you replied and allows you to continue typing into it.
*/
protected final ArraySet<String> mKeysKeptForRemoteInput = new ArraySet<>();
protected final Context mContext;
private final UserManager mUserManager;
@@ -290,7 +284,8 @@ public class NotificationRemoteInputManager implements Dumpable {
mRemoteInputController.addCallback(new RemoteInputController.Callback() {
@Override
public void onRemoteInputSent(NotificationData.Entry entry) {
if (FORCE_REMOTE_INPUT_HISTORY && mKeysKeptForRemoteInput.contains(entry.key)) {
if (FORCE_REMOTE_INPUT_HISTORY
&& mEntryManager.isNotificationKeptForRemoteInput(entry.key)) {
mEntryManager.removeNotification(entry.key, null);
} else if (mRemoteInputEntriesToRemoveOnCollapse.contains(entry)) {
// We're currently holding onto this notification, but from the apps point of
@@ -340,10 +335,6 @@ public class NotificationRemoteInputManager implements Dumpable {
if (mRemoteInputController.isRemoteInputActive(entry)) {
mRemoteInputController.removeRemoteInput(entry, null);
}
if (FORCE_REMOTE_INPUT_HISTORY
&& mKeysKeptForRemoteInput.contains(n.getKey())) {
mKeysKeptForRemoteInput.remove(n.getKey());
}
}
public void removeRemoteInputEntriesKeptUntilCollapsed() {
@@ -368,8 +359,6 @@ public class NotificationRemoteInputManager implements Dumpable {
pw.println("NotificationRemoteInputManager state:");
pw.print(" mRemoteInputEntriesToRemoveOnCollapse: ");
pw.println(mRemoteInputEntriesToRemoveOnCollapse);
pw.print(" mKeysKeptForRemoteInput: ");
pw.println(mKeysKeptForRemoteInput);
}
public void bindRow(ExpandableNotificationRow row) {
@@ -377,10 +366,6 @@ public class NotificationRemoteInputManager implements Dumpable {
row.setRemoteViewClickHandler(mOnClickHandler);
}
public Set<String> getKeysKeptForRemoteInput() {
return mKeysKeptForRemoteInput;
}
@VisibleForTesting
public Set<NotificationData.Entry> getRemoteInputEntriesToRemoveOnCollapse() {
return mRemoteInputEntriesToRemoveOnCollapse;

View File

@@ -17,10 +17,12 @@ package com.android.systemui.statusbar;
import android.os.RemoteException;
import android.service.notification.StatusBarNotification;
import android.util.ArraySet;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dependency;
import java.util.Set;
/**
* Handles when smart replies are added to a notification
@@ -28,18 +30,20 @@ import com.android.systemui.Dependency;
*/
public class SmartReplyController {
private IStatusBarService mBarService;
private NotificationEntryManager mNotificationEntryManager;
private Set<String> mSendingKeys = new ArraySet<>();
public SmartReplyController() {
mBarService = Dependency.get(IStatusBarService.class);
mNotificationEntryManager = Dependency.get(NotificationEntryManager.class);
}
public void smartReplySent(NotificationData.Entry entry, int replyIndex, CharSequence reply) {
NotificationEntryManager notificationEntryManager
= Dependency.get(NotificationEntryManager.class);
StatusBarNotification newSbn =
mNotificationEntryManager.rebuildNotificationWithRemoteInput(entry, reply,
notificationEntryManager.rebuildNotificationWithRemoteInput(entry, reply,
true /* showSpinner */);
mNotificationEntryManager.updateNotification(newSbn, null /* ranking */);
notificationEntryManager.updateNotification(newSbn, null /* ranking */);
mSendingKeys.add(entry.key);
try {
mBarService.onNotificationSmartReplySent(entry.notification.getKey(),
@@ -49,6 +53,14 @@ public class SmartReplyController {
}
}
/**
* Have we posted an intent to an app about sending a smart reply from the
* notification with this key.
*/
public boolean isSendingSmartReply(String key) {
return mSendingKeys.contains(key);
}
public void smartRepliesAdded(final NotificationData.Entry entry, int replyCount) {
try {
mBarService.onNotificationSmartRepliesAdded(entry.notification.getKey(),
@@ -57,4 +69,10 @@ public class SmartReplyController {
// Nothing to do, system going down
}
}
public void stopSending(final NotificationData.Entry entry) {
if (entry != null) {
mSendingKeys.remove(entry.notification.getKey());
}
}
}

View File

@@ -5161,8 +5161,7 @@ public class StatusBar extends SystemUI implements DemoMode,
removeNotification(parentToCancelFinal);
}
if (shouldAutoCancel(sbn)
|| mRemoteInputManager.getKeysKeptForRemoteInput().contains(
notificationKey)) {
|| mEntryManager.isNotificationKeptForRemoteInput(notificationKey)) {
// Automatically remove all notifications that we may have kept around longer
removeNotification(sbn);
}

View File

@@ -207,6 +207,7 @@ public class SmartReplyView extends ViewGroup {
b.setText(choice);
OnDismissAction action = () -> {
smartReplyController.smartReplySent(entry, replyIndex, b.getText());
Bundle results = new Bundle();
results.putString(remoteInput.getResultKey(), choice.toString());
Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
@@ -217,7 +218,6 @@ public class SmartReplyView extends ViewGroup {
} catch (PendingIntent.CanceledException e) {
Log.w(TAG, "Unable to send smart reply", e);
}
smartReplyController.smartReplySent(entry, replyIndex, b.getText());
mSmartReplyContainer.setVisibility(View.GONE);
return false; // do not defer
};

View File

@@ -19,6 +19,7 @@ package com.android.systemui.statusbar;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.assertFalse;
import static org.junit.Assert.assertEquals;
import static org.mockito.ArgumentMatchers.any;
@@ -97,6 +98,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
@Mock private DeviceProvisionedController mDeviceProvisionedController;
@Mock private VisualStabilityManager mVisualStabilityManager;
@Mock private MetricsLogger mMetricsLogger;
@Mock private SmartReplyController mSmartReplyController;
private NotificationData.Entry mEntry;
private StatusBarNotification mSbn;
@@ -158,6 +160,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
mDeviceProvisionedController);
mDependency.injectTestDependency(VisualStabilityManager.class, mVisualStabilityManager);
mDependency.injectTestDependency(MetricsLogger.class, mMetricsLogger);
mDependency.injectTestDependency(SmartReplyController.class, mSmartReplyController);
mCountDownLatch = new CountDownLatch(1);
@@ -262,6 +265,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
verify(mMediaManager).onNotificationRemoved(mSbn.getKey());
verify(mRemoteInputManager).onRemoveNotification(mEntry);
verify(mSmartReplyController).stopSending(mEntry);
verify(mForegroundServiceController).removeNotification(mSbn);
verify(mListContainer).cleanUpViewState(mRow);
verify(mPresenter).updateNotificationViews();
@@ -271,6 +275,20 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
assertNull(mEntryManager.getNotificationData().get(mSbn.getKey()));
}
@Test
public void testRemoveNotification_blockedBySendingSmartReply() throws Exception {
com.android.systemui.util.Assert.isNotMainThread();
mEntry.row = mRow;
mEntryManager.getNotificationData().add(mEntry);
when(mSmartReplyController.isSendingSmartReply(mEntry.key)).thenReturn(true);
mEntryManager.removeNotification(mSbn.getKey(), mRankingMap);
assertNotNull(mEntryManager.getNotificationData().get(mSbn.getKey()));
assertTrue(mEntryManager.isNotificationKeptForRemoteInput(mEntry.key));
}
@Test
public void testUpdateAppOps_foregroundNoti() {
com.android.systemui.util.Assert.isNotMainThread();
@@ -365,6 +383,8 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
Assert.assertEquals("A Reply", messages[0]);
Assert.assertFalse(newSbn.getNotification().extras
.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
Assert.assertTrue(newSbn.getNotification().extras
.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
}
@Test
@@ -377,6 +397,8 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
Assert.assertEquals("A Reply", messages[0]);
Assert.assertTrue(newSbn.getNotification().extras
.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
Assert.assertTrue(newSbn.getNotification().extras
.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
}
@Test
@@ -394,4 +416,15 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
Assert.assertEquals("Reply 2", messages[0]);
Assert.assertEquals("A Reply", messages[1]);
}
@Test
public void testRebuildNotificationForCanceledSmartReplies() {
// Try rebuilding to remove spinner and hide buttons.
StatusBarNotification newSbn =
mEntryManager.rebuildNotificationForCanceledSmartReplies(mEntry);
Assert.assertFalse(newSbn.getNotification().extras
.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false));
Assert.assertTrue(newSbn.getNotification().extras
.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
}
}

View File

@@ -60,7 +60,6 @@ public class NotificationListenerTest extends SysuiTestCase {
private NotificationListener mListener;
private StatusBarNotification mSbn;
private Set<String> mKeysKeptForRemoteInput;
@Before
public void setUp() {
@@ -69,11 +68,8 @@ public class NotificationListenerTest extends SysuiTestCase {
mDependency.injectTestDependency(NotificationRemoteInputManager.class,
mRemoteInputManager);
mKeysKeptForRemoteInput = new HashSet<>();
when(mPresenter.getHandler()).thenReturn(Handler.createAsync(Looper.myLooper()));
when(mEntryManager.getNotificationData()).thenReturn(mNotificationData);
when(mRemoteInputManager.getKeysKeptForRemoteInput()).thenReturn(mKeysKeptForRemoteInput);
mListener = new NotificationListener(mContext);
mSbn = new StatusBarNotification(TEST_PACKAGE_NAME, TEST_PACKAGE_NAME, 0, null, TEST_UID, 0,
@@ -91,10 +87,9 @@ public class NotificationListenerTest extends SysuiTestCase {
@Test
public void testPostNotificationRemovesKeyKeptForRemoteInput() {
mKeysKeptForRemoteInput.add(mSbn.getKey());
mListener.onNotificationPosted(mSbn, mRanking);
TestableLooper.get(this).processAllMessages();
assertTrue(mKeysKeptForRemoteInput.isEmpty());
verify(mEntryManager).removeKeyKeptForRemoteInput(mSbn.getKey());
}
@Test

View File

@@ -86,11 +86,9 @@ public class NotificationRemoteInputManagerTest extends SysuiTestCase {
@Test
public void testPerformOnRemoveNotification() {
when(mController.isRemoteInputActive(mEntry)).thenReturn(true);
mRemoteInputManager.getKeysKeptForRemoteInput().add(mEntry.key);
mRemoteInputManager.onPerformRemoveNotification(mSbn, mEntry);
verify(mController).removeRemoteInput(mEntry, null);
assertTrue(mRemoteInputManager.getKeysKeptForRemoteInput().isEmpty());
}
@Test

View File

@@ -14,6 +14,8 @@
package com.android.systemui.statusbar;
import static junit.framework.Assert.assertFalse;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
@@ -74,7 +76,7 @@ public class SmartReplyControllerTest extends SysuiTestCase {
}
@Test
public void testSendSmartReply_updatesRemoteInput() throws RemoteException {
public void testSendSmartReply_updatesRemoteInput() {
StatusBarNotification sbn = mock(StatusBarNotification.class);
when(sbn.getKey()).thenReturn(TEST_NOTIFICATION_KEY);
when(mNotificationEntryManager.rebuildNotificationWithRemoteInput(
@@ -118,4 +120,21 @@ public class SmartReplyControllerTest extends SysuiTestCase {
verify(mIStatusBarService).onNotificationSmartRepliesAdded(TEST_NOTIFICATION_KEY,
TEST_CHOICE_COUNT);
}
@Test
public void testSendSmartReply_reportsSending() {
SmartReplyController controller = new SmartReplyController();
controller.smartReplySent(mEntry, TEST_CHOICE_INDEX, TEST_CHOICE_TEXT);
assertTrue(controller.isSendingSmartReply(TEST_NOTIFICATION_KEY));
}
@Test
public void testSendingSmartReply_afterRemove_shouldReturnFalse() {
SmartReplyController controller = new SmartReplyController();
controller.isSendingSmartReply(TEST_NOTIFICATION_KEY);
controller.stopSending(mEntry);
assertFalse(controller.isSendingSmartReply(TEST_NOTIFICATION_KEY));
}
}