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"; 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 * The default component of
* {@link android.service.notification.NotificationAssistantService}. * {@link android.service.notification.NotificationAssistantService}.

View File

@@ -469,6 +469,10 @@
--> -->
<integer name="config_smart_replies_in_notifications_max_num_actions">-1</integer> <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. <!-- Screenshot editing default activity. Must handle ACTION_EDIT image/png intents.
Blank sends the user to the Chooser first. Blank sends the user to the Chooser first.
This name is in the ComponentName flattened format (package/class) --> 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.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBar; import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.PrintWriter; import java.io.PrintWriter;
@@ -3194,6 +3195,13 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mAmbientGoingAway = goingAway; mAmbientGoingAway = goingAway;
} }
/**
* Returns the Smart Suggestions backing the smart suggestion buttons in the notification.
*/
public SmartRepliesAndActions getExistingSmartRepliesAndActions() {
return mPrivateLayout.getCurrentSmartRepliesAndActions();
}
@VisibleForTesting @VisibleForTesting
protected void setChildrenContainer(NotificationChildrenContainer childrenContainer) { protected void setChildrenContainer(NotificationChildrenContainer childrenContainer) {
mChildrenContainer = 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.phone.StatusBar;
import com.android.systemui.statusbar.policy.HeadsUpManager; import com.android.systemui.statusbar.policy.HeadsUpManager;
import com.android.systemui.statusbar.policy.InflatedSmartReplies; 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.statusbar.policy.SmartReplyConstants;
import com.android.systemui.util.Assert; import com.android.systemui.util.Assert;
@@ -282,7 +283,8 @@ public class NotificationContentInflater {
mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
mRedactAmbient, packageContext); mRedactAmbient, packageContext);
result = inflateSmartReplyViews(result, reInflateFlags, mRow.getEntry(), result = inflateSmartReplyViews(result, reInflateFlags, mRow.getEntry(),
mRow.getContext(), mRow.getHeadsUpManager()); mRow.getContext(), mRow.getHeadsUpManager(),
mRow.getExistingSmartRepliesAndActions());
apply( apply(
inflateSynchronously, inflateSynchronously,
result, result,
@@ -344,20 +346,20 @@ public class NotificationContentInflater {
private static InflationProgress inflateSmartReplyViews(InflationProgress result, private static InflationProgress inflateSmartReplyViews(InflationProgress result,
@InflationFlag int reInflateFlags, NotificationEntry entry, Context context, @InflationFlag int reInflateFlags, NotificationEntry entry, Context context,
HeadsUpManager headsUpManager) { HeadsUpManager headsUpManager, SmartRepliesAndActions previousSmartRepliesAndActions) {
SmartReplyConstants smartReplyConstants = Dependency.get(SmartReplyConstants.class); SmartReplyConstants smartReplyConstants = Dependency.get(SmartReplyConstants.class);
SmartReplyController smartReplyController = Dependency.get(SmartReplyController.class); SmartReplyController smartReplyController = Dependency.get(SmartReplyController.class);
if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 && result.newExpandedView != null) { if ((reInflateFlags & FLAG_CONTENT_VIEW_EXPANDED) != 0 && result.newExpandedView != null) {
result.expandedInflatedSmartReplies = result.expandedInflatedSmartReplies =
InflatedSmartReplies.inflate( InflatedSmartReplies.inflate(
context, entry, smartReplyConstants, smartReplyController, context, entry, smartReplyConstants, smartReplyController,
headsUpManager); headsUpManager, previousSmartRepliesAndActions);
} }
if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 && result.newHeadsUpView != null) { if ((reInflateFlags & FLAG_CONTENT_VIEW_HEADS_UP) != 0 && result.newHeadsUpView != null) {
result.headsUpInflatedSmartReplies = result.headsUpInflatedSmartReplies =
InflatedSmartReplies.inflate( InflatedSmartReplies.inflate(
context, entry, smartReplyConstants, smartReplyController, context, entry, smartReplyConstants, smartReplyController,
headsUpManager); headsUpManager, previousSmartRepliesAndActions);
} }
return result; return result;
} }
@@ -905,7 +907,8 @@ public class NotificationContentInflater {
mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight,
mRedactAmbient, packageContext); mRedactAmbient, packageContext);
return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mRow.getEntry(), return inflateSmartReplyViews(inflationProgress, mReInflateFlags, mRow.getEntry(),
mRow.getContext(), mRow.getHeadsUpManager()); mRow.getContext(), mRow.getHeadsUpManager(),
mRow.getExistingSmartRepliesAndActions());
} catch (Exception e) { } catch (Exception e) {
mError = e; mError = e;
return null; return null;

View File

@@ -93,6 +93,7 @@ public class NotificationContentView extends FrameLayout {
private SmartReplyController mSmartReplyController; private SmartReplyController mSmartReplyController;
private InflatedSmartReplies mExpandedInflatedSmartReplies; private InflatedSmartReplies mExpandedInflatedSmartReplies;
private InflatedSmartReplies mHeadsUpInflatedSmartReplies; private InflatedSmartReplies mHeadsUpInflatedSmartReplies;
private SmartRepliesAndActions mCurrentSmartRepliesAndActions;
private NotificationViewWrapper mContractedWrapper; private NotificationViewWrapper mContractedWrapper;
private NotificationViewWrapper mExpandedWrapper; private NotificationViewWrapper mExpandedWrapper;
@@ -1259,18 +1260,18 @@ public class NotificationContentView extends FrameLayout {
// the same SmartRepliesAndActions to avoid discrepancies between the two views. We here // the same SmartRepliesAndActions to avoid discrepancies between the two views. We here
// reuse that object for our local SmartRepliesAndActions to avoid discrepancies between // reuse that object for our local SmartRepliesAndActions to avoid discrepancies between
// this class and the InflatedSmartReplies classes. // this class and the InflatedSmartReplies classes.
SmartRepliesAndActions smartRepliesAndActions = mExpandedInflatedSmartReplies != null mCurrentSmartRepliesAndActions = mExpandedInflatedSmartReplies != null
? mExpandedInflatedSmartReplies.getSmartRepliesAndActions() ? mExpandedInflatedSmartReplies.getSmartRepliesAndActions()
: mHeadsUpInflatedSmartReplies.getSmartRepliesAndActions(); : mHeadsUpInflatedSmartReplies.getSmartRepliesAndActions();
if (DEBUG) { if (DEBUG) {
Log.d(TAG, String.format("Adding suggestions for %s, %d actions, and %d replies.", Log.d(TAG, String.format("Adding suggestions for %s, %d actions, and %d replies.",
entry.notification.getKey(), entry.notification.getKey(),
smartRepliesAndActions.smartActions == null ? 0 : mCurrentSmartRepliesAndActions.smartActions == null ? 0 :
smartRepliesAndActions.smartActions.actions.size(), mCurrentSmartRepliesAndActions.smartActions.actions.size(),
smartRepliesAndActions.smartReplies == null ? 0 : mCurrentSmartRepliesAndActions.smartReplies == null ? 0 :
smartRepliesAndActions.smartReplies.choices.length)); mCurrentSmartRepliesAndActions.smartReplies.choices.length));
} }
applySmartReplyView(smartRepliesAndActions, entry); applySmartReplyView(mCurrentSmartRepliesAndActions, entry);
} }
private void applyRemoteInput(NotificationEntry entry, boolean hasFreeformRemoteInput) { 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() { public void closeRemoteInput() {
if (mHeadsUpRemoteInput != null) { if (mHeadsUpRemoteInput != null) {
mHeadsUpRemoteInput.close(); mHeadsUpRemoteInput.close();

View File

@@ -28,15 +28,19 @@ import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.widget.Button; import android.widget.Button;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils; import com.android.internal.util.ArrayUtils;
import com.android.systemui.Dependency; import com.android.systemui.Dependency;
import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.shared.system.DevicePolicyManagerWrapper; import com.android.systemui.shared.system.DevicePolicyManagerWrapper;
import com.android.systemui.shared.system.PackageManagerWrapper; import com.android.systemui.shared.system.PackageManagerWrapper;
import com.android.systemui.statusbar.NotificationUiAdjustment;
import com.android.systemui.statusbar.SmartReplyController; import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
@@ -79,29 +83,52 @@ public class InflatedSmartReplies {
NotificationEntry entry, NotificationEntry entry,
SmartReplyConstants smartReplyConstants, SmartReplyConstants smartReplyConstants,
SmartReplyController smartReplyController, SmartReplyController smartReplyController,
HeadsUpManager headsUpManager) { HeadsUpManager headsUpManager,
SmartRepliesAndActions smartRepliesAndActions = SmartRepliesAndActions existingSmartRepliesAndActions) {
SmartRepliesAndActions newSmartRepliesAndActions =
chooseSmartRepliesAndActions(smartReplyConstants, entry); chooseSmartRepliesAndActions(smartReplyConstants, entry);
if (!shouldShowSmartReplyView(entry, smartRepliesAndActions)) { if (!shouldShowSmartReplyView(entry, newSmartRepliesAndActions)) {
return new InflatedSmartReplies(null /* smartReplyView */, 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); SmartReplyView smartReplyView = SmartReplyView.inflate(context);
List<Button> suggestionButtons = new ArrayList<>(); List<Button> suggestionButtons = new ArrayList<>();
if (smartRepliesAndActions.smartReplies != null) { if (newSmartRepliesAndActions.smartReplies != null) {
suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput( suggestionButtons.addAll(smartReplyView.inflateRepliesFromRemoteInput(
smartRepliesAndActions.smartReplies, smartReplyController, entry)); newSmartRepliesAndActions.smartReplies, smartReplyController, entry,
delayOnClickListener));
} }
if (smartRepliesAndActions.smartActions != null) { if (newSmartRepliesAndActions.smartActions != null) {
suggestionButtons.addAll( suggestionButtons.addAll(
smartReplyView.inflateSmartActions(smartRepliesAndActions.smartActions, smartReplyView.inflateSmartActions(newSmartRepliesAndActions.smartActions,
smartReplyController, entry, headsUpManager)); smartReplyController, entry, headsUpManager,
delayOnClickListener));
} }
return new InflatedSmartReplies(smartReplyView, suggestionButtons, 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.smartReplies = smartReplies;
this.smartActions = smartActions; 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 boolean mDefaultShowInHeadsUp;
private final int mDefaultMinNumSystemGeneratedReplies; private final int mDefaultMinNumSystemGeneratedReplies;
private final int mDefaultMaxNumActions; 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 // 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 // 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 boolean mShowInHeadsUp;
private volatile int mMinNumSystemGeneratedReplies; private volatile int mMinNumSystemGeneratedReplies;
private volatile int mMaxNumActions; private volatile int mMaxNumActions;
private volatile long mOnClickInitDelay;
private final Handler mHandler; private final Handler mHandler;
private final Context mContext; private final Context mContext;
@@ -83,6 +85,8 @@ public final class SmartReplyConstants {
R.integer.config_smart_replies_in_notifications_min_num_system_generated_replies); R.integer.config_smart_replies_in_notifications_min_num_system_generated_replies);
mDefaultMaxNumActions = resources.getInteger( mDefaultMaxNumActions = resources.getInteger(
R.integer.config_smart_replies_in_notifications_max_num_actions); R.integer.config_smart_replies_in_notifications_max_num_actions);
mDefaultOnClickInitDelay = resources.getInteger(
R.integer.config_smart_replies_in_notifications_onclick_init_delay);
registerDeviceConfigListener(); registerDeviceConfigListener();
updateConstants(); updateConstants();
@@ -136,6 +140,10 @@ public final class SmartReplyConstants {
DeviceConfig.NAMESPACE_SYSTEMUI, DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.SSIN_MAX_NUM_ACTIONS, SystemUiDeviceConfigFlags.SSIN_MAX_NUM_ACTIONS,
mDefaultMaxNumActions); mDefaultMaxNumActions);
mOnClickInitDelay = DeviceConfig.getInt(
DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.SSIN_ONCLICK_INIT_DELAY,
mDefaultOnClickInitDelay);
} }
} }
@@ -218,4 +226,12 @@ public final class SmartReplyConstants {
public int getMaxNumActions() { public int getMaxNumActions() {
return mMaxNumActions; 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.InsetDrawable;
import android.graphics.drawable.RippleDrawable; import android.graphics.drawable.RippleDrawable;
import android.os.Bundle; import android.os.Bundle;
import android.os.SystemClock;
import android.text.Layout; import android.text.Layout;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.method.TransformationMethod; import android.text.method.TransformationMethod;
@@ -213,14 +214,16 @@ public class SmartReplyView extends ViewGroup {
*/ */
public List<Button> inflateRepliesFromRemoteInput( public List<Button> inflateRepliesFromRemoteInput(
@NonNull SmartReplies smartReplies, @NonNull SmartReplies smartReplies,
SmartReplyController smartReplyController, NotificationEntry entry) { SmartReplyController smartReplyController, NotificationEntry entry,
boolean delayOnClickListener) {
List<Button> buttons = new ArrayList<>(); List<Button> buttons = new ArrayList<>();
if (smartReplies.remoteInput != null && smartReplies.pendingIntent != null) { if (smartReplies.remoteInput != null && smartReplies.pendingIntent != null) {
if (smartReplies.choices != null) { if (smartReplies.choices != null) {
for (int i = 0; i < smartReplies.choices.length; ++i) { for (int i = 0; i < smartReplies.choices.length; ++i) {
buttons.add(inflateReplyButton( buttons.add(inflateReplyButton(
this, getContext(), i, smartReplies, smartReplyController, entry)); this, getContext(), i, smartReplies, smartReplyController, entry,
delayOnClickListener));
} }
this.mSmartRepliesGeneratedByAssistant = smartReplies.fromAssistant; this.mSmartRepliesGeneratedByAssistant = smartReplies.fromAssistant;
} }
@@ -234,7 +237,7 @@ public class SmartReplyView extends ViewGroup {
*/ */
public List<Button> inflateSmartActions(@NonNull SmartActions smartActions, public List<Button> inflateSmartActions(@NonNull SmartActions smartActions,
SmartReplyController smartReplyController, NotificationEntry entry, SmartReplyController smartReplyController, NotificationEntry entry,
HeadsUpManager headsUpManager) { HeadsUpManager headsUpManager, boolean delayOnClickListener) {
List<Button> buttons = new ArrayList<>(); List<Button> buttons = new ArrayList<>();
int numSmartActions = smartActions.actions.size(); int numSmartActions = smartActions.actions.size();
for (int n = 0; n < numSmartActions; n++) { for (int n = 0; n < numSmartActions; n++) {
@@ -242,7 +245,7 @@ public class SmartReplyView extends ViewGroup {
if (action.actionIntent != null) { if (action.actionIntent != null) {
buttons.add(inflateActionButton( buttons.add(inflateActionButton(
this, getContext(), n, smartActions, smartReplyController, entry, this, getContext(), n, smartActions, smartReplyController, entry,
headsUpManager)); headsUpManager, delayOnClickListener));
} }
} }
return buttons; return buttons;
@@ -259,7 +262,7 @@ public class SmartReplyView extends ViewGroup {
@VisibleForTesting @VisibleForTesting
static Button inflateReplyButton(SmartReplyView smartReplyView, Context context, static Button inflateReplyButton(SmartReplyView smartReplyView, Context context,
int replyIndex, SmartReplies smartReplies, SmartReplyController smartReplyController, int replyIndex, SmartReplies smartReplies, SmartReplyController smartReplyController,
NotificationEntry entry) { NotificationEntry entry, boolean useDelayedOnClickListener) {
Button b = (Button) LayoutInflater.from(context).inflate( Button b = (Button) LayoutInflater.from(context).inflate(
R.layout.smart_reply_button, smartReplyView, false); R.layout.smart_reply_button, smartReplyView, false);
CharSequence choice = smartReplies.choices[replyIndex]; CharSequence choice = smartReplies.choices[replyIndex];
@@ -299,9 +302,13 @@ public class SmartReplyView extends ViewGroup {
return false; // do not defer return false; // do not defer
}; };
b.setOnClickListener(view -> { OnClickListener onClickListener = view ->
smartReplyView.mKeyguardDismissUtil.executeWhenUnlocked(action); smartReplyView.mKeyguardDismissUtil.executeWhenUnlocked(action);
}); if (useDelayedOnClickListener) {
onClickListener = new DelayedOnClickListener(onClickListener,
smartReplyView.mConstants.getOnClickInitDelay());
}
b.setOnClickListener(onClickListener);
b.setAccessibilityDelegate(new AccessibilityDelegate() { b.setAccessibilityDelegate(new AccessibilityDelegate() {
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
@@ -322,7 +329,7 @@ public class SmartReplyView extends ViewGroup {
static Button inflateActionButton(SmartReplyView smartReplyView, Context context, static Button inflateActionButton(SmartReplyView smartReplyView, Context context,
int actionIndex, SmartActions smartActions, int actionIndex, SmartActions smartActions,
SmartReplyController smartReplyController, NotificationEntry entry, SmartReplyController smartReplyController, NotificationEntry entry,
HeadsUpManager headsUpManager) { HeadsUpManager headsUpManager, boolean useDelayedOnClickListener) {
Notification.Action action = smartActions.actions.get(actionIndex); Notification.Action action = smartActions.actions.get(actionIndex);
Button button = (Button) LayoutInflater.from(context).inflate( Button button = (Button) LayoutInflater.from(context).inflate(
R.layout.smart_action_button, smartReplyView, false); R.layout.smart_action_button, smartReplyView, false);
@@ -335,14 +342,19 @@ public class SmartReplyView extends ViewGroup {
iconDrawable.setBounds(0, 0, newIconSize, newIconSize); iconDrawable.setBounds(0, 0, newIconSize, newIconSize);
button.setCompoundDrawables(iconDrawable, null, null, null); button.setCompoundDrawables(iconDrawable, null, null, null);
button.setOnClickListener(view -> OnClickListener onClickListener = view ->
smartReplyView.getActivityStarter().startPendingIntentDismissingKeyguard( smartReplyView.getActivityStarter().startPendingIntentDismissingKeyguard(
action.actionIntent, action.actionIntent,
() -> { () -> {
smartReplyController.smartActionClicked( smartReplyController.smartActionClicked(
entry, actionIndex, action, smartActions.fromAssistant); entry, actionIndex, action, smartActions.fromAssistant);
headsUpManager.removeNotification(entry.key, true); headsUpManager.removeNotification(entry.key, true);
})); });
if (useDelayedOnClickListener) {
onClickListener = new DelayedOnClickListener(onClickListener,
smartReplyView.mConstants.getOnClickInitDelay());
}
button.setOnClickListener(onClickListener);
// Mark this as an Action button // Mark this as an Action button
final LayoutParams lp = (LayoutParams) button.getLayoutParams(); final LayoutParams lp = (LayoutParams) button.getLayoutParams();
@@ -958,4 +970,32 @@ public class SmartReplyView extends ViewGroup {
this.fromAssistant = fromAssistant; 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 { public Intent waitForIntent() throws InterruptedException {
return mQueue.poll(10, TimeUnit.SECONDS); 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.shared.system.PackageManagerWrapper;
import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions; 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.Before;
import org.junit.Test; import org.junit.Test;
@@ -51,6 +53,7 @@ import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
@SmallTest @SmallTest
@@ -322,6 +325,72 @@ public class InflatedSmartRepliesTest extends SysuiTestCase {
mEntry.systemGeneratedSmartActions); 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) { private void setupAppGeneratedReplies(CharSequence[] smartReplies) {
setupAppGeneratedReplies(smartReplies, true /* allowSystemGeneratedReplies */); setupAppGeneratedReplies(smartReplies, true /* allowSystemGeneratedReplies */);
} }

View File

@@ -211,6 +211,18 @@ public class SmartReplyConstantsTest extends SysuiTestCase {
assertEquals(10, mConstants.getMaxNumActions()); 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) { private void overrideSetting(String propertyName, String value) {
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
propertyName, value, false /* makeDefault */); propertyName, value, false /* makeDefault */);
@@ -239,5 +251,7 @@ public class SmartReplyConstantsTest extends SysuiTestCase {
false /* makeDefault */); false /* makeDefault */);
DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI, DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
SystemUiDeviceConfigFlags.SSIN_MAX_NUM_ACTIONS, null, false /* makeDefault */); 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.ArgumentMatchers.any;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times; import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -120,6 +121,8 @@ public class SmartReplyViewTest extends SysuiTestCase {
when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(0); when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(0);
when(mConstants.getMaxSqueezeRemeasureAttempts()).thenReturn(3); when(mConstants.getMaxSqueezeRemeasureAttempts()).thenReturn(3);
when(mConstants.getMaxNumActions()).thenReturn(-1); 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(); final Resources res = mContext.getResources();
mSingleLinePaddingHorizontal = res.getDimensionPixelSize( mSingleLinePaddingHorizontal = res.getDimensionPixelSize(
@@ -164,7 +167,7 @@ public class SmartReplyViewTest extends SysuiTestCase {
mView.getChildAt(2).performClick(); mView.getChildAt(2).performClick();
assertNull(mReceiver.waitForIntent()); assertNull(mReceiver.waitForIntentShortDelay());
} }
@Test @Test
@@ -176,7 +179,7 @@ public class SmartReplyViewTest extends SysuiTestCase {
mView.getChildAt(2).performClick(); mView.getChildAt(2).performClick();
// No intent until the screen is unlocked. // No intent until the screen is unlocked.
assertNull(mReceiver.waitForIntent()); assertNull(mReceiver.waitForIntentShortDelay());
actionRef.get().onDismiss(); actionRef.get().onDismiss();
@@ -203,6 +206,48 @@ public class SmartReplyViewTest extends SysuiTestCase {
assertEquals(View.GONE, mContainer.getVisibility()); 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 @Test
public void testMeasure_empty() { public void testMeasure_empty() {
mView.measure(WIDTH_SPEC, HEIGHT_SPEC); mView.measure(WIDTH_SPEC, HEIGHT_SPEC);
@@ -403,18 +448,25 @@ public class SmartReplyViewTest extends SysuiTestCase {
} }
private void setSmartReplies(CharSequence[] choices) { private void setSmartReplies(CharSequence[] choices) {
setSmartReplies(choices, true /* useDelayedOnClickListener */);
}
private void setSmartReplies(CharSequence[] choices, boolean useDelayedOnClickListener) {
mView.resetSmartSuggestions(mContainer); mView.resetSmartSuggestions(mContainer);
List<Button> replyButtons = inflateSmartReplies(choices, false /* fromAssistant */); List<Button> replyButtons = inflateSmartReplies(choices, false /* fromAssistant */,
useDelayedOnClickListener);
mView.addPreInflatedButtons(replyButtons); 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, PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
new Intent(TEST_ACTION), 0); new Intent(TEST_ACTION), 0);
RemoteInput input = new RemoteInput.Builder(TEST_RESULT_KEY).setChoices(choices).build(); RemoteInput input = new RemoteInput.Builder(TEST_RESULT_KEY).setChoices(choices).build();
SmartReplyView.SmartReplies smartReplies = SmartReplyView.SmartReplies smartReplies =
new SmartReplyView.SmartReplies(choices, input, pendingIntent, fromAssistant); 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) { private Notification.Action createAction(String actionTitle) {
@@ -432,28 +484,37 @@ public class SmartReplyViewTest extends SysuiTestCase {
} }
private void setSmartActions(String[] actionTitles) { private void setSmartActions(String[] actionTitles) {
setSmartActions(actionTitles, true /* useDelayedOnClickListener */);
}
private void setSmartActions(String[] actionTitles, boolean useDelayedOnClickListener) {
mView.resetSmartSuggestions(mContainer); mView.resetSmartSuggestions(mContainer);
List<Button> actions = mView.inflateSmartActions( List<Button> actions = mView.inflateSmartActions(
new SmartReplyView.SmartActions(createActions(actionTitles), false), new SmartReplyView.SmartActions(createActions(actionTitles), false),
mLogger, mLogger,
mEntry, mEntry,
mHeadsUpManager); mHeadsUpManager,
useDelayedOnClickListener);
mView.addPreInflatedButtons(actions); mView.addPreInflatedButtons(actions);
} }
private void setSmartRepliesAndActions(CharSequence[] choices, String[] actionTitles) { private void setSmartRepliesAndActions(CharSequence[] choices, String[] actionTitles) {
setSmartRepliesAndActions(choices, actionTitles, false /* fromAssistant */); setSmartRepliesAndActions(choices, actionTitles, false /* fromAssistant */,
true /* useDelayedOnClickListener */);
} }
private void setSmartRepliesAndActions( private void setSmartRepliesAndActions(
CharSequence[] choices, String[] actionTitles, boolean fromAssistant) { CharSequence[] choices, String[] actionTitles, boolean fromAssistant,
boolean useDelayedOnClickListener) {
mView.resetSmartSuggestions(mContainer); mView.resetSmartSuggestions(mContainer);
List<Button> smartSuggestions = inflateSmartReplies(choices, fromAssistant); List<Button> smartSuggestions = inflateSmartReplies(choices, fromAssistant,
useDelayedOnClickListener);
smartSuggestions.addAll(mView.inflateSmartActions( smartSuggestions.addAll(mView.inflateSmartActions(
new SmartReplyView.SmartActions(createActions(actionTitles), fromAssistant), new SmartReplyView.SmartActions(createActions(actionTitles), fromAssistant),
mLogger, mLogger,
mEntry, mEntry,
mHeadsUpManager)); mHeadsUpManager,
useDelayedOnClickListener));
mView.addPreInflatedButtons(smartSuggestions); mView.addPreInflatedButtons(smartSuggestions);
} }
@@ -491,7 +552,8 @@ public class SmartReplyViewTest extends SysuiTestCase {
new SmartReplyView.SmartReplies(choices, null, null, false); new SmartReplyView.SmartReplies(choices, null, null, false);
for (int i = 0; i < choices.length; ++i) { for (int i = 0; i < choices.length; ++i) {
Button current = SmartReplyView.inflateReplyButton(mView, mContext, i, smartReplies, 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.setPadding(paddingHorizontal, current.getPaddingTop(), paddingHorizontal,
current.getPaddingBottom()); current.getPaddingBottom());
if (previous != null) { if (previous != null) {
@@ -575,6 +637,40 @@ public class SmartReplyViewTest extends SysuiTestCase {
verify(mActivityStarter, times(1)).startPendingIntentDismissingKeyguard(any(), any()); 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 @Test
public void testMeasure_shortSmartActions() { public void testMeasure_shortSmartActions() {
String[] actions = new String[] {"Hi", "Hello", "Bye"}; String[] actions = new String[] {"Hi", "Hello", "Bye"};
@@ -759,7 +855,7 @@ public class SmartReplyViewTest extends SysuiTestCase {
private Button inflateActionButton(Notification.Action action) { private Button inflateActionButton(Notification.Action action) {
return SmartReplyView.inflateActionButton(mView, getContext(), 0, return SmartReplyView.inflateActionButton(mView, getContext(), 0,
new SmartReplyView.SmartActions(Collections.singletonList(action), false), new SmartReplyView.SmartActions(Collections.singletonList(action), false),
mLogger, mEntry, mHeadsUpManager); mLogger, mEntry, mHeadsUpManager, true /* useDelayedOnClickListener */);
} }
@Test @Test
@@ -965,7 +1061,8 @@ public class SmartReplyViewTest extends SysuiTestCase {
createActions(new String[] {"action1"})); createActions(new String[] {"action1"}));
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
setSmartRepliesAndActions(choices, actions, true /* fromAssistant */); setSmartRepliesAndActions(
choices, actions, true /* fromAssistant */, true /* useDelayedOnClickListener */);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
assertEqualMeasures(expectedView, mView); assertEqualMeasures(expectedView, mView);
@@ -988,7 +1085,8 @@ public class SmartReplyViewTest extends SysuiTestCase {
createActions(new String[] {"action1"})); createActions(new String[] {"action1"}));
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
setSmartRepliesAndActions(choices, actions, true /* fromAssistant */); setSmartRepliesAndActions(
choices, actions, true /* fromAssistant */, true /* useDelayedOnClickListener */);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
assertEqualMeasures(expectedView, mView); assertEqualMeasures(expectedView, mView);
@@ -1017,7 +1115,8 @@ public class SmartReplyViewTest extends SysuiTestCase {
createActions(new String[] {"Short action"})); createActions(new String[] {"Short action"}));
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
setSmartRepliesAndActions(choices, actions, true /* fromAssistant */); setSmartRepliesAndActions(
choices, actions, true /* fromAssistant */, true /* useDelayedOnClickListener */);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
assertEqualMeasures(expectedView, mView); assertEqualMeasures(expectedView, mView);