Block clicks on smart actions and replies just after creation/update.
To avoid accidental clicks on smart actions and replies we here block
clicks on those buttons just after they are created. We block clicks
on those buttons when a notification is updated - but only if the
buttons are new, or different from previous buttons shown in the
notification. I.e. if the notification is updated but the smart
suggestion buttons stay the same we don't block clicks on them.
Bug: 128683184
Test: manually ensure clicks are blocked within the initialization
delay (for new / changed buttons), and ensure the delay changes
when calling
adb shell device_config put systemui ssin_onclick_init_delay X
where X is the delay in ms.
Test: SmartReplyConstantsTest, SmartReplyViewTest
Change-Id: I9a44eb6ade6579a42e35b36cce4bd5863332c60e
diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml
index e02be38..99a75d2 100644
--- a/packages/SystemUI/res/values/config.xml
+++ b/packages/SystemUI/res/values/config.xml
@@ -469,6 +469,10 @@
-->
<integer name="config_smart_replies_in_notifications_max_num_actions">-1</integer>
+ <!-- Smart replies in notifications: Delay (ms) before smart suggestions are clickable, since
+ they were added. -->
+ <integer name="config_smart_replies_in_notifications_onclick_init_delay">200</integer>
+
<!-- Screenshot editing default activity. Must handle ACTION_EDIT image/png intents.
Blank sends the user to the Chooser first.
This name is in the ComponentName flattened format (package/class) -->
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
index efdcd05..6b2efaab 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java
@@ -100,6 +100,7 @@
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.HeadsUpManager;
+import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -3194,6 +3195,13 @@
mAmbientGoingAway = goingAway;
}
+ /**
+ * Returns the Smart Suggestions backing the smart suggestion buttons in the notification.
+ */
+ public SmartRepliesAndActions getExistingSmartRepliesAndActions() {
+ return mPrivateLayout.getCurrentSmartRepliesAndActions();
+ }
+
@VisibleForTesting
protected void setChildrenContainer(NotificationChildrenContainer childrenContainer) {
mChildrenContainer = childrenContainer;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
index 09f513d..f095b90 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java
@@ -44,6 +44,7 @@
import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.InflatedSmartReplies;
+import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.util.Assert;
@@ -284,7 +285,8 @@
mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
mRedactAmbient, packageContext);
result = inflateSmartReplyViews(result, reInflateFlags, mRow.getEntry(),
- mRow.getContext(), mRow.getHeadsUpManager());
+ mRow.getContext(), mRow.getHeadsUpManager(),
+ mRow.getExistingSmartRepliesAndActions());
apply(
inflateSynchronously,
result,
@@ -346,20 +348,20 @@
private static InflationProgress inflateSmartReplyViews(InflationProgress result,
@InflationFlag int reInflateFlags, NotificationEntry entry, Context context,
- HeadsUpManager headsUpManager) {
+ HeadsUpManager headsUpManager, SmartRepliesAndActions previousSmartRepliesAndActions) {
SmartReplyConstants smartReplyConstants = Dependency.get(SmartReplyConstants.class);
SmartReplyController smartReplyController = Dependency.get(SmartReplyController.class);
if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 && result.newExpandedView != null) {
result.expandedInflatedSmartReplies =
InflatedSmartReplies.inflate(
context, entry, smartReplyConstants, smartReplyController,
- headsUpManager);
+ headsUpManager, previousSmartRepliesAndActions);
}
if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 && result.newHeadsUpView != null) {
result.headsUpInflatedSmartReplies =
InflatedSmartReplies.inflate(
context, entry, smartReplyConstants, smartReplyController,
- headsUpManager);
+ headsUpManager, previousSmartRepliesAndActions);
}
return result;
}
@@ -907,7 +909,8 @@
mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
mRedactAmbient, packageContext);
return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mRow.getEntry(),
- mRow.getContext(), mRow.getHeadsUpManager());
+ mRow.getContext(), mRow.getHeadsUpManager(),
+ mRow.getExistingSmartRepliesAndActions());
} catch (Exception e) {
mError = e;
return null;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index 7850035..b81d814 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -93,6 +93,7 @@
private SmartReplyController mSmartReplyController;
private InflatedSmartReplies mExpandedInflatedSmartReplies;
private InflatedSmartReplies mHeadsUpInflatedSmartReplies;
+ private SmartRepliesAndActions mCurrentSmartRepliesAndActions;
private NotificationViewWrapper mContractedWrapper;
private NotificationViewWrapper mExpandedWrapper;
@@ -1259,18 +1260,18 @@
// the same SmartRepliesAndActions to avoid discrepancies between the two views. We here
// reuse that object for our local SmartRepliesAndActions to avoid discrepancies between
// this class and the InflatedSmartReplies classes.
- SmartRepliesAndActions smartRepliesAndActions = mExpandedInflatedSmartReplies != null
+ mCurrentSmartRepliesAndActions = mExpandedInflatedSmartReplies != null
? mExpandedInflatedSmartReplies.getSmartRepliesAndActions()
: mHeadsUpInflatedSmartReplies.getSmartRepliesAndActions();
if (DEBUG) {
Log.d(TAG, String.format("Adding suggestions for %s, %d actions, and %d replies.",
entry.notification.getKey(),
- smartRepliesAndActions.smartActions == null ? 0 :
- smartRepliesAndActions.smartActions.actions.size(),
- smartRepliesAndActions.smartReplies == null ? 0 :
- smartRepliesAndActions.smartReplies.choices.length));
+ mCurrentSmartRepliesAndActions.smartActions == null ? 0 :
+ mCurrentSmartRepliesAndActions.smartActions.actions.size(),
+ mCurrentSmartRepliesAndActions.smartReplies == null ? 0 :
+ mCurrentSmartRepliesAndActions.smartReplies.choices.length));
}
- applySmartReplyView(smartRepliesAndActions, entry);
+ applySmartReplyView(mCurrentSmartRepliesAndActions, entry);
}
private void applyRemoteInput(NotificationEntry entry, boolean hasFreeformRemoteInput) {
@@ -1472,6 +1473,13 @@
}
}
+ /**
+ * Returns the smart replies and actions currently shown in the notification.
+ */
+ @Nullable public SmartRepliesAndActions getCurrentSmartRepliesAndActions() {
+ return mCurrentSmartRepliesAndActions;
+ }
+
public void closeRemoteInput() {
if (mHeadsUpRemoteInput != null) {
mHeadsUpRemoteInput.close();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java
index 5b2e398..ee78a72 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/InflatedSmartReplies.java
@@ -28,15 +28,19 @@
import android.util.Pair;
import android.widget.Button;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.systemui.Dependency;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
import com.android.systemui.shared.system.PackageManagerWrapper;
+import com.android.systemui.statusbar.NotificationUiAdjustment;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
/**
@@ -79,29 +83,52 @@
NotificationEntry entry,
SmartReplyConstants smartReplyConstants,
SmartReplyController smartReplyController,
- HeadsUpManager headsUpManager) {
- SmartRepliesAndActions smartRepliesAndActions =
+ HeadsUpManager headsUpManager,
+ SmartRepliesAndActions existingSmartRepliesAndActions) {
+ SmartRepliesAndActions newSmartRepliesAndActions =
chooseSmartRepliesAndActions(smartReplyConstants, entry);
- if (!shouldShowSmartReplyView(entry, smartRepliesAndActions)) {
+ if (!shouldShowSmartReplyView(entry, newSmartRepliesAndActions)) {
return new InflatedSmartReplies(null /* smartReplyView */,
- null /* smartSuggestionButtons */, smartRepliesAndActions);
+ null /* smartSuggestionButtons */, newSmartRepliesAndActions);
}
+ // Only block clicks if the smart buttons are different from the previous set - to avoid
+ // scenarios where a user incorrectly cannot click smart buttons because the notification is
+ // updated.
+ boolean delayOnClickListener =
+ !areSuggestionsSimilar(existingSmartRepliesAndActions, newSmartRepliesAndActions);
+
SmartReplyView smartReplyView = SmartReplyView.inflate(context);
List<Button> suggestionButtons = new ArrayList<>();
- if (smartRepliesAndActions.smartReplies != null) {
+ if (newSmartRepliesAndActions.smartReplies != null) {
suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput(
- smartRepliesAndActions.smartReplies, smartReplyController, entry));
+ newSmartRepliesAndActions.smartReplies, smartReplyController, entry,
+ delayOnClickListener));
}
- if (smartRepliesAndActions.smartActions != null) {
+ if (newSmartRepliesAndActions.smartActions != null) {
suggestionButtons.addAll(
- smartReplyView.inflateSmartActions(smartRepliesAndActions.smartActions,
- smartReplyController, entry, headsUpManager));
+ smartReplyView.inflateSmartActions(newSmartRepliesAndActions.smartActions,
+ smartReplyController, entry, headsUpManager,
+ delayOnClickListener));
}
return new InflatedSmartReplies(smartReplyView, suggestionButtons,
- smartRepliesAndActions);
+ newSmartRepliesAndActions);
+ }
+
+ @VisibleForTesting
+ static boolean areSuggestionsSimilar(
+ SmartRepliesAndActions left, SmartRepliesAndActions right) {
+ if (left == right) return true;
+ if (left == null || right == null) return false;
+
+ if (!Arrays.equals(left.getSmartReplies(), right.getSmartReplies())) {
+ return false;
+ }
+
+ return !NotificationUiAdjustment.areDifferent(
+ left.getSmartActions(), right.getSmartActions());
}
/**
@@ -260,5 +287,13 @@
this.smartReplies = smartReplies;
this.smartActions = smartActions;
}
+
+ @NonNull public CharSequence[] getSmartReplies() {
+ return smartReplies == null ? new CharSequence[0] : smartReplies.choices;
+ }
+
+ @NonNull public List<Notification.Action> getSmartActions() {
+ return smartActions == null ? Collections.emptyList() : smartActions.actions;
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java
index e57cff7..f02b544 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java
@@ -47,6 +47,7 @@
private final boolean mDefaultShowInHeadsUp;
private final int mDefaultMinNumSystemGeneratedReplies;
private final int mDefaultMaxNumActions;
+ private final int mDefaultOnClickInitDelay;
// These fields are updated on the UI thread but can be accessed on both the UI thread and
// background threads. We use the volatile keyword here instead of synchronization blocks since
@@ -59,6 +60,7 @@
private volatile boolean mShowInHeadsUp;
private volatile int mMinNumSystemGeneratedReplies;
private volatile int mMaxNumActions;
+ private volatile long mOnClickInitDelay;
private final Handler mHandler;
private final Context mContext;
@@ -83,6 +85,8 @@
R.integer.config_smart_replies_in_notifications_min_num_system_generated_replies);
mDefaultMaxNumActions = resources.getInteger(
R.integer.config_smart_replies_in_notifications_max_num_actions);
+ mDefaultOnClickInitDelay = resources.getInteger(
+ R.integer.config_smart_replies_in_notifications_onclick_init_delay);
registerDeviceConfigListener();
updateConstants();
@@ -136,6 +140,10 @@
DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.SSIN_MAX_NUM_ACTIONS,
mDefaultMaxNumActions);
+ mOnClickInitDelay = DeviceConfig.getInt(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.SSIN_ONCLICK_INIT_DELAY,
+ mDefaultOnClickInitDelay);
}
}
@@ -218,4 +226,12 @@
public int getMaxNumActions() {
return mMaxNumActions;
}
+
+ /**
+ * Returns the amount of time (ms) before smart suggestions are clickable, since the suggestions
+ * were added.
+ */
+ public long getOnClickInitDelay() {
+ return mOnClickInitDelay;
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
index ed5487f..0f7a0f0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
@@ -16,6 +16,7 @@
import android.graphics.drawable.InsetDrawable;
import android.graphics.drawable.RippleDrawable;
import android.os.Bundle;
+import android.os.SystemClock;
import android.text.Layout;
import android.text.TextPaint;
import android.text.method.TransformationMethod;
@@ -213,14 +214,16 @@
*/
public List<Button> inflateRepliesFromRemoteInput(
@NonNull SmartReplies smartReplies,
- SmartReplyController smartReplyController, NotificationEntry entry) {
+ SmartReplyController smartReplyController, NotificationEntry entry,
+ boolean delayOnClickListener) {
List<Button> buttons = new ArrayList<>();
if (smartReplies.remoteInput != null && smartReplies.pendingIntent != null) {
if (smartReplies.choices != null) {
for (int i = 0; i < smartReplies.choices.length; ++i) {
buttons.add(inflateReplyButton(
- this, getContext(), i, smartReplies, smartReplyController, entry));
+ this, getContext(), i, smartReplies, smartReplyController, entry,
+ delayOnClickListener));
}
this.mSmartRepliesGeneratedByAssistant = smartReplies.fromAssistant;
}
@@ -234,7 +237,7 @@
*/
public List<Button> inflateSmartActions(@NonNull SmartActions smartActions,
SmartReplyController smartReplyController, NotificationEntry entry,
- HeadsUpManager headsUpManager) {
+ HeadsUpManager headsUpManager, boolean delayOnClickListener) {
List<Button> buttons = new ArrayList<>();
int numSmartActions = smartActions.actions.size();
for (int n = 0; n < numSmartActions; n++) {
@@ -242,7 +245,7 @@
if (action.actionIntent != null) {
buttons.add(inflateActionButton(
this, getContext(), n, smartActions, smartReplyController, entry,
- headsUpManager));
+ headsUpManager, delayOnClickListener));
}
}
return buttons;
@@ -259,7 +262,7 @@
@VisibleForTesting
static Button inflateReplyButton(SmartReplyView smartReplyView, Context context,
int replyIndex, SmartReplies smartReplies, SmartReplyController smartReplyController,
- NotificationEntry entry) {
+ NotificationEntry entry, boolean useDelayedOnClickListener) {
Button b = (Button) LayoutInflater.from(context).inflate(
R.layout.smart_reply_button, smartReplyView, false);
CharSequence choice = smartReplies.choices[replyIndex];
@@ -299,9 +302,13 @@
return false; // do not defer
};
- b.setOnClickListener(view -> {
+ OnClickListener onClickListener = view ->
smartReplyView.mKeyguardDismissUtil.executeWhenUnlocked(action);
- });
+ if (useDelayedOnClickListener) {
+ onClickListener = new DelayedOnClickListener(onClickListener,
+ smartReplyView.mConstants.getOnClickInitDelay());
+ }
+ b.setOnClickListener(onClickListener);
b.setAccessibilityDelegate(new AccessibilityDelegate() {
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
@@ -322,7 +329,7 @@
static Button inflateActionButton(SmartReplyView smartReplyView, Context context,
int actionIndex, SmartActions smartActions,
SmartReplyController smartReplyController, NotificationEntry entry,
- HeadsUpManager headsUpManager) {
+ HeadsUpManager headsUpManager, boolean useDelayedOnClickListener) {
Notification.Action action = smartActions.actions.get(actionIndex);
Button button = (Button) LayoutInflater.from(context).inflate(
R.layout.smart_action_button, smartReplyView, false);
@@ -335,14 +342,19 @@
iconDrawable.setBounds(0, 0, newIconSize, newIconSize);
button.setCompoundDrawables(iconDrawable, null, null, null);
- button.setOnClickListener(view ->
+ OnClickListener onClickListener = view ->
smartReplyView.getActivityStarter().startPendingIntentDismissingKeyguard(
action.actionIntent,
() -> {
smartReplyController.smartActionClicked(
entry, actionIndex, action, smartActions.fromAssistant);
headsUpManager.removeNotification(entry.key, true);
- }));
+ });
+ if (useDelayedOnClickListener) {
+ onClickListener = new DelayedOnClickListener(onClickListener,
+ smartReplyView.mConstants.getOnClickInitDelay());
+ }
+ button.setOnClickListener(onClickListener);
// Mark this as an Action button
final LayoutParams lp = (LayoutParams) button.getLayoutParams();
@@ -958,4 +970,32 @@
this.fromAssistant = fromAssistant;
}
}
+
+ /**
+ * An OnClickListener wrapper that blocks the underlying OnClickListener for a given amount of
+ * time.
+ */
+ private static class DelayedOnClickListener implements OnClickListener {
+ private final OnClickListener mActualListener;
+ private final long mInitDelayMs;
+ private final long mInitTimeMs;
+
+ DelayedOnClickListener(OnClickListener actualOnClickListener, long initDelayMs) {
+ mActualListener = actualOnClickListener;
+ mInitDelayMs = initDelayMs;
+ mInitTimeMs = SystemClock.elapsedRealtime();
+ }
+
+ public void onClick(View v) {
+ if (hasFinishedInitialization()) {
+ mActualListener.onClick(v);
+ } else {
+ Log.i(TAG, "Accidental Smart Suggestion click registered, delay: " + mInitDelayMs);
+ }
+ }
+
+ private boolean hasFinishedInitialization() {
+ return SystemClock.elapsedRealtime() >= mInitTimeMs + mInitDelayMs;
+ }
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BlockingQueueIntentReceiver.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BlockingQueueIntentReceiver.java
index 76a3c95..7c46298 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BlockingQueueIntentReceiver.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/BlockingQueueIntentReceiver.java
@@ -34,4 +34,8 @@
public Intent waitForIntent() throws InterruptedException {
return mQueue.poll(10, TimeUnit.SECONDS);
}
+
+ public Intent waitForIntentShortDelay() throws InterruptedException {
+ return mQueue.poll(3, TimeUnit.SECONDS);
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java
index de1072d..2462fb3 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/InflatedSmartRepliesTest.java
@@ -43,6 +43,8 @@
import com.android.systemui.shared.system.PackageManagerWrapper;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions;
+import com.android.systemui.statusbar.policy.SmartReplyView.SmartActions;
+import com.android.systemui.statusbar.policy.SmartReplyView.SmartReplies;
import org.junit.Before;
import org.junit.Test;
@@ -51,6 +53,7 @@
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
@SmallTest
@@ -322,6 +325,72 @@
mEntry.systemGeneratedSmartActions);
}
+ @Test
+ public void areSuggestionsSimilar_trueForSimilar() {
+ CharSequence[] leftReplies = new CharSequence[] { "first reply", "second reply"};
+ CharSequence[] rightReplies = new CharSequence[] { "first reply", "second reply"};
+ List<Notification.Action> leftActions = Arrays.asList(
+ createAction("firstAction"),
+ createAction("secondAction"));
+ List<Notification.Action> rightActions = Arrays.asList(
+ createAction("firstAction"),
+ createAction("secondAction"));
+
+ SmartRepliesAndActions leftRepliesAndActions = new SmartRepliesAndActions(
+ new SmartReplies(leftReplies, null, null, false /* fromAssistant */),
+ new SmartActions(leftActions, false /* fromAssistant */));
+ SmartRepliesAndActions rightRepliesAndActions = new SmartRepliesAndActions(
+ new SmartReplies(rightReplies, null, null, false /* fromAssistant */),
+ new SmartActions(rightActions, false /* fromAssistant */));
+
+ assertThat(InflatedSmartReplies.areSuggestionsSimilar(
+ leftRepliesAndActions, rightRepliesAndActions)).isTrue();
+ }
+
+ @Test
+ public void areSuggestionsSimilar_falseForDifferentReplies() {
+ CharSequence[] leftReplies = new CharSequence[] { "first reply"};
+ CharSequence[] rightReplies = new CharSequence[] { "first reply", "second reply"};
+ List<Notification.Action> leftActions = Arrays.asList(
+ createAction("firstAction"),
+ createAction("secondAction"));
+ List<Notification.Action> rightActions = Arrays.asList(
+ createAction("firstAction"),
+ createAction("secondAction"));
+
+ SmartRepliesAndActions leftRepliesAndActions = new SmartRepliesAndActions(
+ new SmartReplies(leftReplies, null, null, false /* fromAssistant */),
+ new SmartActions(leftActions, false /* fromAssistant */));
+ SmartRepliesAndActions rightRepliesAndActions = new SmartRepliesAndActions(
+ new SmartReplies(rightReplies, null, null, false /* fromAssistant */),
+ new SmartActions(rightActions, false /* fromAssistant */));
+
+ assertThat(InflatedSmartReplies.areSuggestionsSimilar(
+ leftRepliesAndActions, rightRepliesAndActions)).isFalse();
+ }
+
+ @Test
+ public void areSuggestionsSimilar_falseForDifferentActions() {
+ CharSequence[] leftReplies = new CharSequence[] { "first reply", "second reply"};
+ CharSequence[] rightReplies = new CharSequence[] { "first reply", "second reply"};
+ List<Notification.Action> leftActions = Arrays.asList(
+ createAction("firstAction"),
+ createAction("secondAction"));
+ List<Notification.Action> rightActions = Arrays.asList(
+ createAction("firstAction"),
+ createAction("not secondAction"));
+
+ SmartRepliesAndActions leftRepliesAndActions = new SmartRepliesAndActions(
+ new SmartReplies(leftReplies, null, null, false /* fromAssistant */),
+ new SmartActions(leftActions, false /* fromAssistant */));
+ SmartRepliesAndActions rightRepliesAndActions = new SmartRepliesAndActions(
+ new SmartReplies(rightReplies, null, null, false /* fromAssistant */),
+ new SmartActions(rightActions, false /* fromAssistant */));
+
+ assertThat(InflatedSmartReplies.areSuggestionsSimilar(
+ leftRepliesAndActions, rightRepliesAndActions)).isFalse();
+ }
+
private void setupAppGeneratedReplies(CharSequence[] smartReplies) {
setupAppGeneratedReplies(smartReplies, true /* allowSystemGeneratedReplies */);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java
index 3edfb56..fb2b7dc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyConstantsTest.java
@@ -211,6 +211,18 @@
assertEquals(10, mConstants.getMaxNumActions());
}
+ @Test
+ public void testOnClickInitDelayWithNoConfig() {
+ assertEquals(200, mConstants.getOnClickInitDelay());
+ }
+
+ @Test
+ public void testOnClickInitDelaySet() {
+ overrideSetting(SystemUiDeviceConfigFlags.SSIN_ONCLICK_INIT_DELAY, "50");
+ triggerConstantsOnChange();
+ assertEquals(50, mConstants.getOnClickInitDelay());
+ }
+
private void overrideSetting(String propertyName, String value) {
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
propertyName, value, false /* makeDefault */);
@@ -239,5 +251,7 @@
false /* makeDefault */);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.SSIN_MAX_NUM_ACTIONS, null, false /* makeDefault */);
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.SSIN_ONCLICK_INIT_DELAY, null, false /* makeDefault */);
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
index 0ce1df3..01f3c92 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
@@ -24,6 +24,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -120,6 +121,8 @@
when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(0);
when(mConstants.getMaxSqueezeRemeasureAttempts()).thenReturn(3);
when(mConstants.getMaxNumActions()).thenReturn(-1);
+ // Ensure there's no delay before we can click smart suggestions.
+ when(mConstants.getOnClickInitDelay()).thenReturn(0L);
final Resources res = mContext.getResources();
mSingleLinePaddingHorizontal = res.getDimensionPixelSize(
@@ -164,7 +167,7 @@
mView.getChildAt(2).performClick();
- assertNull(mReceiver.waitForIntent());
+ assertNull(mReceiver.waitForIntentShortDelay());
}
@Test
@@ -176,7 +179,7 @@
mView.getChildAt(2).performClick();
// No intent until the screen is unlocked.
- assertNull(mReceiver.waitForIntent());
+ assertNull(mReceiver.waitForIntentShortDelay());
actionRef.get().onDismiss();
@@ -204,6 +207,48 @@
}
@Test
+ public void testTapSmartReply_beforeInitDelay_blocked() throws InterruptedException {
+ // 100 seconds is easily enough for our click to always be blocked.
+ when(mConstants.getOnClickInitDelay()).thenReturn(100L * 1000L);
+ setSmartReplies(TEST_CHOICES);
+
+ mView.getChildAt(2).performClick();
+
+ assertNull(mReceiver.waitForIntentShortDelay());
+ }
+
+ @Test
+ public void testTapSmartReply_afterInitDelay_clickReceived() throws InterruptedException {
+ final long delayMs = 50L; // Using a small delay to not delay the test suite too much.
+ when(mConstants.getOnClickInitDelay()).thenReturn(delayMs);
+ setSmartReplies(TEST_CHOICES);
+
+ Thread.sleep(delayMs);
+ mView.getChildAt(2).performClick();
+
+ // Now the intent should arrive.
+ Intent resultIntent = mReceiver.waitForIntent();
+ assertEquals(TEST_CHOICES[2],
+ RemoteInput.getResultsFromIntent(resultIntent).get(TEST_RESULT_KEY));
+ assertEquals(RemoteInput.SOURCE_CHOICE, RemoteInput.getResultsSource(resultIntent));
+ }
+
+ @Test
+ public void testTapSmartReply_withoutDelayedOnClickListener_bypassesDelay()
+ throws InterruptedException {
+ // 100 seconds is easily enough for our click to always be blocked.
+ when(mConstants.getOnClickInitDelay()).thenReturn(100L * 1000L);
+ setSmartReplies(TEST_CHOICES, false /* useDelayedOnClickListener */);
+
+ mView.getChildAt(2).performClick();
+
+ Intent resultIntent = mReceiver.waitForIntent();
+ assertEquals(TEST_CHOICES[2],
+ RemoteInput.getResultsFromIntent(resultIntent).get(TEST_RESULT_KEY));
+ assertEquals(RemoteInput.SOURCE_CHOICE, RemoteInput.getResultsSource(resultIntent));
+ }
+
+ @Test
public void testMeasure_empty() {
mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
assertEquals(500, mView.getMeasuredWidthAndState());
@@ -403,18 +448,25 @@
}
private void setSmartReplies(CharSequence[] choices) {
+ setSmartReplies(choices, true /* useDelayedOnClickListener */);
+ }
+
+ private void setSmartReplies(CharSequence[] choices, boolean useDelayedOnClickListener) {
mView.resetSmartSuggestions(mContainer);
- List<Button> replyButtons = inflateSmartReplies(choices, false /* fromAssistant */);
+ List<Button> replyButtons = inflateSmartReplies(choices, false /* fromAssistant */,
+ useDelayedOnClickListener);
mView.addPreInflatedButtons(replyButtons);
}
- private List<Button> inflateSmartReplies(CharSequence[] choices, boolean fromAssistant) {
+ private List<Button> inflateSmartReplies(CharSequence[] choices, boolean fromAssistant,
+ boolean useDelayedOnClickListener) {
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
new Intent(TEST_ACTION), 0);
RemoteInput input = new RemoteInput.Builder(TEST_RESULT_KEY).setChoices(choices).build();
SmartReplyView.SmartReplies smartReplies =
new SmartReplyView.SmartReplies(choices, input, pendingIntent, fromAssistant);
- return mView.inflateRepliesFromRemoteInput(smartReplies, mLogger, mEntry);
+ return mView.inflateRepliesFromRemoteInput(smartReplies, mLogger, mEntry,
+ useDelayedOnClickListener);
}
private Notification.Action createAction(String actionTitle) {
@@ -432,28 +484,37 @@
}
private void setSmartActions(String[] actionTitles) {
+ setSmartActions(actionTitles, true /* useDelayedOnClickListener */);
+ }
+
+ private void setSmartActions(String[] actionTitles, boolean useDelayedOnClickListener) {
mView.resetSmartSuggestions(mContainer);
List<Button> actions = mView.inflateSmartActions(
new SmartReplyView.SmartActions(createActions(actionTitles), false),
mLogger,
mEntry,
- mHeadsUpManager);
+ mHeadsUpManager,
+ useDelayedOnClickListener);
mView.addPreInflatedButtons(actions);
}
private void setSmartRepliesAndActions(CharSequence[] choices, String[] actionTitles) {
- setSmartRepliesAndActions(choices, actionTitles, false /* fromAssistant */);
+ setSmartRepliesAndActions(choices, actionTitles, false /* fromAssistant */,
+ true /* useDelayedOnClickListener */);
}
private void setSmartRepliesAndActions(
- CharSequence[] choices, String[] actionTitles, boolean fromAssistant) {
+ CharSequence[] choices, String[] actionTitles, boolean fromAssistant,
+ boolean useDelayedOnClickListener) {
mView.resetSmartSuggestions(mContainer);
- List<Button> smartSuggestions = inflateSmartReplies(choices, fromAssistant);
+ List<Button> smartSuggestions = inflateSmartReplies(choices, fromAssistant,
+ useDelayedOnClickListener);
smartSuggestions.addAll(mView.inflateSmartActions(
new SmartReplyView.SmartActions(createActions(actionTitles), fromAssistant),
mLogger,
mEntry,
- mHeadsUpManager));
+ mHeadsUpManager,
+ useDelayedOnClickListener));
mView.addPreInflatedButtons(smartSuggestions);
}
@@ -491,7 +552,8 @@
new SmartReplyView.SmartReplies(choices, null, null, false);
for (int i = 0; i < choices.length; ++i) {
Button current = SmartReplyView.inflateReplyButton(mView, mContext, i, smartReplies,
- null /* SmartReplyController */, null /* NotificationEntry */);
+ null /* SmartReplyController */, null /* NotificationEntry */,
+ true /* useDelayedOnClickListener */);
current.setPadding(paddingHorizontal, current.getPaddingTop(), paddingHorizontal,
current.getPaddingBottom());
if (previous != null) {
@@ -576,6 +638,40 @@
}
@Test
+ public void testTapSmartAction_beforeInitDelay_blocked() throws InterruptedException {
+ // 100 seconds is easily enough for our click to always be blocked.
+ when(mConstants.getOnClickInitDelay()).thenReturn(100L * 1000L);
+ setSmartActions(TEST_ACTION_TITLES);
+
+ mView.getChildAt(2).performClick();
+
+ verify(mActivityStarter, never()).startPendingIntentDismissingKeyguard(any(), any());
+ }
+
+ @Test
+ public void testTapSmartAction_afterInitDelay_clickReceived() throws InterruptedException {
+ final long delayMs = 50L; // Using a small delay to not delay the test suite too much.
+ when(mConstants.getOnClickInitDelay()).thenReturn(delayMs);
+ setSmartActions(TEST_ACTION_TITLES);
+
+ Thread.sleep(delayMs);
+ mView.getChildAt(2).performClick();
+
+ verify(mActivityStarter, times(1)).startPendingIntentDismissingKeyguard(any(), any());
+ }
+
+ @Test
+ public void testTapSmartAction_withoutDelayedOnClickListener_bypassesDelay() {
+ // 100 seconds is easily enough for our click to always be blocked.
+ when(mConstants.getOnClickInitDelay()).thenReturn(100L * 1000L);
+ setSmartActions(TEST_ACTION_TITLES, false /* useDelayedOnClickListener */);
+
+ mView.getChildAt(2).performClick();
+
+ verify(mActivityStarter, times(1)).startPendingIntentDismissingKeyguard(any(), any());
+ }
+
+ @Test
public void testMeasure_shortSmartActions() {
String[] actions = new String[] {"Hi", "Hello", "Bye"};
// All choices should be displayed as SINGLE-line smart action buttons.
@@ -759,7 +855,7 @@
private Button inflateActionButton(Notification.Action action) {
return SmartReplyView.inflateActionButton(mView, getContext(), 0,
new SmartReplyView.SmartActions(Collections.singletonList(action), false),
- mLogger, mEntry, mHeadsUpManager);
+ mLogger, mEntry, mHeadsUpManager, true /* useDelayedOnClickListener */);
}
@Test
@@ -965,7 +1061,8 @@
createActions(new String[] {"action1"}));
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- setSmartRepliesAndActions(choices, actions, true /* fromAssistant */);
+ setSmartRepliesAndActions(
+ choices, actions, true /* fromAssistant */, true /* useDelayedOnClickListener */);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
assertEqualMeasures(expectedView, mView);
@@ -988,7 +1085,8 @@
createActions(new String[] {"action1"}));
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- setSmartRepliesAndActions(choices, actions, true /* fromAssistant */);
+ setSmartRepliesAndActions(
+ choices, actions, true /* fromAssistant */, true /* useDelayedOnClickListener */);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
assertEqualMeasures(expectedView, mView);
@@ -1017,7 +1115,8 @@
createActions(new String[] {"Short action"}));
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- setSmartRepliesAndActions(choices, actions, true /* fromAssistant */);
+ setSmartRepliesAndActions(
+ choices, actions, true /* fromAssistant */, true /* useDelayedOnClickListener */);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
assertEqualMeasures(expectedView, mView);