Merge "Block clicks on smart actions and replies just after creation/update." into qt-dev

This commit is contained in:
TreeHugger Robot
2019-04-20 22:15:21 +00:00
committed by Android (Google) Code Review
12 changed files with 352 additions and 46 deletions

View File

@@ -81,6 +81,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}.

View File

@@ -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) -->

View File

@@ -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;

View File

@@ -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;

View File

@@ -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();

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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 */);
}

View File

@@ -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 */);
}
}

View File

@@ -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();
@@ -203,6 +206,48 @@ public class SmartReplyViewTest extends SysuiTestCase {
assertEquals(View.GONE, mContainer.getVisibility());
}
@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);
@@ -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) {
@@ -575,6 +637,40 @@ public class SmartReplyViewTest extends SysuiTestCase {
verify(mActivityStarter, times(1)).startPendingIntentDismissingKeyguard(any(), any());
}
@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"};
@@ -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);