diff options
12 files changed, 352 insertions, 46 deletions
diff --git a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java index 495a5fbb6665..6d0a8646b3a7 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiDeviceConfigFlags.java @@ -82,6 +82,12 @@ public final class SystemUiDeviceConfigFlags { public static final String SSIN_MAX_NUM_ACTIONS = "ssin_max_num_actions"; /** + * (int) The amount of time (ms) before smart suggestions are clickable, since the suggestions + * were added. + */ + public static final String SSIN_ONCLICK_INIT_DELAY = "ssin_onclick_init_delay"; + + /** * The default component of * {@link android.service.notification.NotificationAssistantService}. */ diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index d6906f3988cd..73386879a20d 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 efdcd053bc54..6b2efaab0a64 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.notification.stack.NotificationStackScroll 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 @@ public class ExpandableNotificationRow extends ActivatableNotificationView 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 18dcf4c55d21..396cd73f9a22 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.notification.row.wrapper.NotificationViewW 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; @@ -282,7 +283,8 @@ public class NotificationContentInflater { mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient, packageContext); result = inflateSmartReplyViews(result, reInflateFlags, mRow.getEntry(), - mRow.getContext(), mRow.getHeadsUpManager()); + mRow.getContext(), mRow.getHeadsUpManager(), + mRow.getExistingSmartRepliesAndActions()); apply( inflateSynchronously, result, @@ -344,20 +346,20 @@ public class NotificationContentInflater { 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; } @@ -905,7 +907,8 @@ public class NotificationContentInflater { 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 78500357f41f..b81d81438ea3 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 @@ public class NotificationContentView extends FrameLayout { private SmartReplyController mSmartReplyController; private InflatedSmartReplies mExpandedInflatedSmartReplies; private InflatedSmartReplies mHeadsUpInflatedSmartReplies; + private SmartRepliesAndActions mCurrentSmartRepliesAndActions; private NotificationViewWrapper mContractedWrapper; private NotificationViewWrapper mExpandedWrapper; @@ -1259,18 +1260,18 @@ public class NotificationContentView extends FrameLayout { // 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 @@ public class NotificationContentView extends FrameLayout { } } + /** + * 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 5b2e398b66e1..ee78a723a49c 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.Log; 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 @@ public class InflatedSmartReplies { 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 @@ public class InflatedSmartReplies { 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 e57cff789a23..f02b5441a737 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 @@ public final class SmartReplyConstants { 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 @@ public final class SmartReplyConstants { 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 @@ public final class SmartReplyConstants { 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 @@ public final class SmartReplyConstants { 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 final class SmartReplyConstants { 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 ed5487f74356..0f7a0f09b2e1 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.GradientDrawable; 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 class SmartReplyView extends ViewGroup { */ 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 class SmartReplyView extends ViewGroup { */ 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 @@ public class SmartReplyView extends ViewGroup { if (action.actionIntent != null) { buttons.add(inflateActionButton( this, getContext(), n, smartActions, smartReplyController, entry, - headsUpManager)); + headsUpManager, delayOnClickListener)); } } return buttons; @@ -259,7 +262,7 @@ public class SmartReplyView extends ViewGroup { @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 @@ public class SmartReplyView extends ViewGroup { 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 @@ public class SmartReplyView extends ViewGroup { 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 @@ public class SmartReplyView extends ViewGroup { 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 @@ public class SmartReplyView extends ViewGroup { 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 76a3c95cad0a..7c4629871658 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 class BlockingQueueIntentReceiver extends BroadcastReceiver { 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 de1072d450f7..2462fb3ca589 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.DevicePolicyManagerWrapper; 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.Mock; import org.mockito.MockitoAnnotations; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; @SmallTest @@ -322,6 +325,72 @@ public class InflatedSmartRepliesTest extends SysuiTestCase { 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 3edfb56fbaac..fb2b7dced7c2 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 @@ public class SmartReplyConstantsTest extends SysuiTestCase { 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 @@ public class SmartReplyConstantsTest extends SysuiTestCase { 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 0ce1df3d19f4..01f3c923832f 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 junit.framework.Assert.fail; 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 @@ public class SmartReplyViewTest extends SysuiTestCase { 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 @@ public class SmartReplyViewTest extends SysuiTestCase { mView.getChildAt(2).performClick(); - assertNull(mReceiver.waitForIntent()); + assertNull(mReceiver.waitForIntentShortDelay()); } @Test @@ -176,7 +179,7 @@ public class SmartReplyViewTest extends SysuiTestCase { mView.getChildAt(2).performClick(); // No intent until the screen is unlocked. - assertNull(mReceiver.waitForIntent()); + assertNull(mReceiver.waitForIntentShortDelay()); actionRef.get().onDismiss(); @@ -204,6 +207,48 @@ public class SmartReplyViewTest extends SysuiTestCase { } @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 @@ public class SmartReplyViewTest extends SysuiTestCase { } 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 @@ public class SmartReplyViewTest extends SysuiTestCase { } 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 @@ public class SmartReplyViewTest extends SysuiTestCase { 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 @@ public class SmartReplyViewTest extends SysuiTestCase { } @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 @@ public class SmartReplyViewTest extends SysuiTestCase { 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 @@ public class SmartReplyViewTest extends SysuiTestCase { 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 @@ public class SmartReplyViewTest extends SysuiTestCase { 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 @@ public class SmartReplyViewTest extends SysuiTestCase { 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); |