diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d5de13c401947..9a03fab0dfdaa 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -13883,11 +13883,12 @@ public final class Settings { * The following keys are supported: * *
-         * enabled                         (boolean)
-         * requires_targeting_p            (boolean)
-         * max_squeeze_remeasure_attempts  (int)
-         * edit_choices_before_sending     (boolean)
-         * show_in_heads_up                (boolean)
+         * enabled                           (boolean)
+         * requires_targeting_p              (boolean)
+         * max_squeeze_remeasure_attempts    (int)
+         * edit_choices_before_sending       (boolean)
+         * show_in_heads_up                  (boolean)
+         * min_num_system_generated_replies  (int)
          * 
* @see com.android.systemui.statusbar.policy.SmartReplyConstants * @hide diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 98f0cbe291106..f2be2e7d8f28a 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -458,6 +458,11 @@ heads-up notifications. --> true + + 0 + 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 3bd0d456dbd30..db0462095dafa 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyConstants.java @@ -46,18 +46,21 @@ public final class SmartReplyConstants extends ContentObserver { private static final String KEY_EDIT_CHOICES_BEFORE_SENDING = "edit_choices_before_sending"; private static final String KEY_SHOW_IN_HEADS_UP = "show_in_heads_up"; + private static final String KEY_MIN_NUM_REPLIES = "min_num_system_generated_replies"; private final boolean mDefaultEnabled; private final boolean mDefaultRequiresP; private final int mDefaultMaxSqueezeRemeasureAttempts; private final boolean mDefaultEditChoicesBeforeSending; private final boolean mDefaultShowInHeadsUp; + private final int mDefaultMinNumSystemGeneratedReplies; private boolean mEnabled; private boolean mRequiresTargetingP; private int mMaxSqueezeRemeasureAttempts; private boolean mEditChoicesBeforeSending; private boolean mShowInHeadsUp; + private int mMinNumSystemGeneratedReplies; private final Context mContext; private final KeyValueListParser mParser = new KeyValueListParser(','); @@ -78,6 +81,8 @@ public final class SmartReplyConstants extends ContentObserver { R.bool.config_smart_replies_in_notifications_edit_choices_before_sending); mDefaultShowInHeadsUp = resources.getBoolean( R.bool.config_smart_replies_in_notifications_show_in_heads_up); + mDefaultMinNumSystemGeneratedReplies = resources.getInteger( + R.integer.config_smart_replies_in_notifications_min_num_system_generated_replies); mContext.getContentResolver().registerContentObserver( Settings.Global.getUriFor(Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS), @@ -105,6 +110,8 @@ public final class SmartReplyConstants extends ContentObserver { mEditChoicesBeforeSending = mParser.getBoolean( KEY_EDIT_CHOICES_BEFORE_SENDING, mDefaultEditChoicesBeforeSending); mShowInHeadsUp = mParser.getBoolean(KEY_SHOW_IN_HEADS_UP, mDefaultShowInHeadsUp); + mMinNumSystemGeneratedReplies = + mParser.getInt(KEY_MIN_NUM_REPLIES, mDefaultMinNumSystemGeneratedReplies); } } @@ -155,4 +162,12 @@ public final class SmartReplyConstants extends ContentObserver { public boolean getShowInHeadsUp() { return mShowInHeadsUp; } + + /** + * Returns the minimum number of system generated replies to show in a notification. + * If we cannot show at least this many system generated replies we should show none. + */ + public int getMinNumSystemGeneratedReplies() { + return mMinNumSystemGeneratedReplies; + } } 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 d6eff941ed70f..c4f027f2abea1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java @@ -88,6 +88,12 @@ public class SmartReplyView extends ViewGroup { private View mSmartReplyContainer; + /** + * Whether the smart replies in this view were generated by the notification assistant. If not + * they're provided by the app. + */ + private boolean mSmartRepliesGeneratedByAssistant = false; + @ColorInt private int mCurrentBackgroundColor; @ColorInt @@ -202,6 +208,7 @@ public class SmartReplyView extends ViewGroup { getContext(), this, i, smartReplies, smartReplyController, entry); addView(replyButton); } + this.mSmartRepliesGeneratedByAssistant = smartReplies.fromAssistant; } } reallocateCandidateButtonQueueForSqueezing(); @@ -344,10 +351,11 @@ public class SmartReplyView extends ViewGroup { mCandidateButtonQueueForSqueezing.clear(); } - int measuredWidth = mPaddingLeft + mPaddingRight; - int maxChildHeight = 0; + SmartSuggestionMeasures accumulatedMeasures = new SmartSuggestionMeasures( + mPaddingLeft + mPaddingRight, + 0 /* maxChildHeight */, + mSingleLineButtonPaddingHorizontal); int displayedChildCount = 0; - int buttonPaddingHorizontal = mSingleLineButtonPaddingHorizontal; // Set up a list of suggestions where actions come before replies. Note that the Buttons // themselves have already been added to the view hierarchy in an order such that Smart @@ -360,11 +368,15 @@ public class SmartReplyView extends ViewGroup { smartSuggestions.addAll(smartReplies); List coveredSuggestions = new ArrayList<>(); + // SmartSuggestionMeasures for all action buttons, this will be filled in when the first + // reply button is added. + SmartSuggestionMeasures actionsMeasures = null; + for (View child : smartSuggestions) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); - child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(), - buttonPaddingHorizontal, child.getPaddingBottom()); + child.setPadding(accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingTop(), + accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingBottom()); child.measure(MEASURE_SPEC_ANY_LENGTH, heightMeasureSpec); coveredSuggestions.add(child); @@ -380,45 +392,52 @@ public class SmartReplyView extends ViewGroup { } // Remember the current measurements in case the current button doesn't fit in. - final int originalMaxChildHeight = maxChildHeight; - final int originalMeasuredWidth = measuredWidth; - final int originalButtonPaddingHorizontal = buttonPaddingHorizontal; + SmartSuggestionMeasures originalMeasures = accumulatedMeasures.clone(); + if (actionsMeasures == null && lp.buttonType == SmartButtonType.REPLY) { + // We've added all actions (we go through actions first), now add their + // measurements. + actionsMeasures = accumulatedMeasures.clone(); + } final int spacing = displayedChildCount == 0 ? 0 : mSpacing; final int childWidth = child.getMeasuredWidth(); final int childHeight = child.getMeasuredHeight(); - measuredWidth += spacing + childWidth; - maxChildHeight = Math.max(maxChildHeight, childHeight); + accumulatedMeasures.mMeasuredWidth += spacing + childWidth; + accumulatedMeasures.mMaxChildHeight = + Math.max(accumulatedMeasures.mMaxChildHeight, childHeight); // Do we need to increase the number of lines in smart reply buttons to two? final boolean increaseToTwoLines = - buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal - && (lineCount == 2 || measuredWidth > targetWidth); + (accumulatedMeasures.mButtonPaddingHorizontal + == mSingleLineButtonPaddingHorizontal) + && (lineCount == 2 || accumulatedMeasures.mMeasuredWidth > targetWidth); if (increaseToTwoLines) { - measuredWidth += (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease; - buttonPaddingHorizontal = mDoubleLineButtonPaddingHorizontal; + accumulatedMeasures.mMeasuredWidth += + (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease; + accumulatedMeasures.mButtonPaddingHorizontal = + mDoubleLineButtonPaddingHorizontal; } // If the last button doesn't fit into the remaining width, try squeezing preceding // smart reply buttons. - if (measuredWidth > targetWidth) { + if (accumulatedMeasures.mMeasuredWidth > targetWidth) { // Keep squeezing preceding and current smart reply buttons until they all fit. - while (measuredWidth > targetWidth + while (accumulatedMeasures.mMeasuredWidth > targetWidth && !mCandidateButtonQueueForSqueezing.isEmpty()) { final Button candidate = mCandidateButtonQueueForSqueezing.poll(); final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec); if (squeezeReduction != SQUEEZE_FAILED) { - maxChildHeight = Math.max(maxChildHeight, candidate.getMeasuredHeight()); - measuredWidth -= squeezeReduction; + accumulatedMeasures.mMaxChildHeight = + Math.max(accumulatedMeasures.mMaxChildHeight, + candidate.getMeasuredHeight()); + accumulatedMeasures.mMeasuredWidth -= squeezeReduction; } } // If the current button still doesn't fit after squeezing all buttons, undo the // last squeezing round. - if (measuredWidth > targetWidth) { - measuredWidth = originalMeasuredWidth; - maxChildHeight = originalMaxChildHeight; - buttonPaddingHorizontal = originalButtonPaddingHorizontal; + if (accumulatedMeasures.mMeasuredWidth > targetWidth) { + accumulatedMeasures = originalMeasures; // Mark all buttons from the last squeezing round as "failed to squeeze", so // that they're re-measured without squeezing later. @@ -440,16 +459,75 @@ public class SmartReplyView extends ViewGroup { displayedChildCount++; } + if (mSmartRepliesGeneratedByAssistant) { + if (!gotEnoughSmartReplies(smartReplies)) { + // We don't have enough smart replies - hide all of them. + for (View smartReplyButton : smartReplies) { + final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams(); + lp.show = false; + } + // Reset our measures back to when we had only added actions (before adding + // replies). + accumulatedMeasures = actionsMeasures; + } + } + // We're done squeezing buttons, so we can clear the priority queue. mCandidateButtonQueueForSqueezing.clear(); // Finally, we need to re-measure some buttons. - remeasureButtonsIfNecessary(buttonPaddingHorizontal, maxChildHeight); + remeasureButtonsIfNecessary(accumulatedMeasures.mButtonPaddingHorizontal, + accumulatedMeasures.mMaxChildHeight); setMeasuredDimension( - resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec), - resolveSize(Math.max(getSuggestedMinimumHeight(), - mPaddingTop + maxChildHeight + mPaddingBottom), heightMeasureSpec)); + resolveSize(Math.max(getSuggestedMinimumWidth(), + accumulatedMeasures.mMeasuredWidth), + widthMeasureSpec), + resolveSize(Math.max(getSuggestedMinimumHeight(), mPaddingTop + + accumulatedMeasures.mMaxChildHeight + mPaddingBottom), + heightMeasureSpec)); + } + + /** + * Fields we keep track of inside onMeasure() to correctly measure the SmartReplyView depending + * on which suggestions are added. + */ + private static class SmartSuggestionMeasures { + int mMeasuredWidth = -1; + int mMaxChildHeight = -1; + int mButtonPaddingHorizontal = -1; + + SmartSuggestionMeasures(int measuredWidth, int maxChildHeight, + int buttonPaddingHorizontal) { + this.mMeasuredWidth = measuredWidth; + this.mMaxChildHeight = maxChildHeight; + this.mButtonPaddingHorizontal = buttonPaddingHorizontal; + } + + public SmartSuggestionMeasures clone() { + return new SmartSuggestionMeasures( + mMeasuredWidth, mMaxChildHeight, mButtonPaddingHorizontal); + } + } + + /** + * Returns whether our notification contains at least N smart replies (or 0) where N is + * determined by {@link SmartReplyConstants}. + */ + private boolean gotEnoughSmartReplies(List smartReplies) { + int numShownReplies = 0; + for (View smartReplyButton : smartReplies) { + final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams(); + if (lp.show) { + numShownReplies++; + } + } + if (numShownReplies == 0 + || numShownReplies >= mConstants.getMinNumSystemGeneratedReplies()) { + // We have enough replies, yay! + return true; + } + return false; } private List filterActionsOrReplies(SmartButtonType buttonType) { 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 3cbf902f1d40b..03b7c9507dc77 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 @@ -55,6 +55,9 @@ public class SmartReplyConstantsTest extends SysuiTestCase { resources.addOverride( R.bool.config_smart_replies_in_notifications_edit_choices_before_sending, false); resources.addOverride(R.bool.config_smart_replies_in_notifications_show_in_heads_up, true); + resources.addOverride( + R.integer.config_smart_replies_in_notifications_min_num_system_generated_replies, + 2); mConstants = new SmartReplyConstants(Handler.createAsync(Looper.myLooper()), mContext); } @@ -178,6 +181,19 @@ public class SmartReplyConstantsTest extends SysuiTestCase { Settings.Global.SMART_REPLIES_IN_NOTIFICATIONS_FLAGS, flags); } + @Test + public void testGetMinNumSystemGeneratedRepliesWithNoConfig() { + assertTrue(mConstants.isEnabled()); + assertEquals(2, mConstants.getMinNumSystemGeneratedReplies()); + } + + @Test + public void testGetMinNumSystemGeneratedRepliesWithValidConfig() { + overrideSetting("enabled=true,min_num_system_generated_replies=5"); + triggerConstantsOnChange(); + assertEquals(5, mConstants.getMinNumSystemGeneratedReplies()); + } + private void triggerConstantsOnChange() { // Since Settings.Global is mocked in TestableContext, we need to manually trigger the // content observer. 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 1066bc1a04dfc..d1c4d0134f6c3 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 @@ -96,6 +96,7 @@ public class SmartReplyViewTest extends SysuiTestCase { @Mock private SmartReplyController mLogger; private NotificationEntry mEntry; private Notification mNotification; + @Mock private SmartReplyConstants mConstants; @Mock ActivityStarter mActivityStarter; @Mock HeadsUpManager mHeadsUpManager; @@ -108,10 +109,14 @@ public class SmartReplyViewTest extends SysuiTestCase { mDependency.get(KeyguardDismissUtil.class).setDismissHandler(action -> action.onDismiss()); mDependency.injectMockDependency(ShadeController.class); mDependency.injectTestDependency(ActivityStarter.class, mActivityStarter); + mDependency.injectTestDependency(SmartReplyConstants.class, mConstants); mContainer = new View(mContext, null); mView = SmartReplyView.inflate(mContext, null); + // Any number of replies are fine. + when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(0); + when(mConstants.getMaxSqueezeRemeasureAttempts()).thenReturn(3); final Resources res = mContext.getResources(); mSingleLinePaddingHorizontal = res.getDimensionPixelSize( @@ -403,7 +408,7 @@ public class SmartReplyViewTest extends SysuiTestCase { } private void setSmartReplies(CharSequence[] choices) { - setSmartReplies(choices, false); + setSmartReplies(choices, false /* fromAssistant */); } private void setSmartReplies(CharSequence[] choices, boolean fromAssistant) { @@ -440,9 +445,14 @@ public class SmartReplyViewTest extends SysuiTestCase { } private void setSmartRepliesAndActions(CharSequence[] choices, String[] actionTitles) { - setSmartReplies(choices); + setSmartRepliesAndActions(choices, actionTitles, false /* fromAssistant */); + } + + private void setSmartRepliesAndActions( + CharSequence[] choices, String[] actionTitles, boolean fromAssistant) { + setSmartReplies(choices, fromAssistant); mView.addSmartActions( - new SmartReplyView.SmartActions(createActions(actionTitles), false), + new SmartReplyView.SmartActions(createActions(actionTitles), fromAssistant), mLogger, mEntry, mHeadsUpManager); @@ -943,4 +953,78 @@ public class SmartReplyViewTest extends SysuiTestCase { expectedView.getChildAt(3), mView.getChildAt(4)); // a1 assertReplyButtonHidden(mView.getChildAt(5)); // long action } + + @Test + public void testMeasure_minNumSystemGeneratedSmartReplies_notEnoughReplies() { + when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(3); + + // Add 2 replies when the minimum is 3 -> we should end up with 0 replies. + String[] choices = new String[] {"reply1", "reply2"}; + String[] actions = new String[] {"action1"}; + + ViewGroup expectedView = buildExpectedView(new String[] {}, 1, + createActions(new String[] {"action1"})); + expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + + setSmartRepliesAndActions(choices, actions, true /* fromAssistant */); + mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + + assertEqualMeasures(expectedView, mView); + // smart replies + assertReplyButtonHidden(mView.getChildAt(0)); + assertReplyButtonHidden(mView.getChildAt(1)); + // smart actions + assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(2)); + } + + @Test + public void testMeasure_minNumSystemGeneratedSmartReplies_enoughReplies() { + when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(2); + + // Add 2 replies when the minimum is 3 -> we should end up with 0 replies. + String[] choices = new String[] {"reply1", "reply2"}; + String[] actions = new String[] {"action1"}; + + ViewGroup expectedView = buildExpectedView(new String[] {"reply1", "reply2"}, 1, + createActions(new String[] {"action1"})); + expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + + setSmartRepliesAndActions(choices, actions, true /* fromAssistant */); + mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + + assertEqualMeasures(expectedView, mView); + // smart replies + assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(0)); + assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(1), mView.getChildAt(1)); + // smart actions + assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(2), mView.getChildAt(2)); + } + + /** + * Ensure actions that are squeezed when shown together with smart replies are unsqueezed if the + * replies are never added (because of the SmartReplyConstants.getMinNumSystemGeneratedReplies() + * flag). + */ + @Test + public void testMeasure_minNumSystemGeneratedSmartReplies_unSqueezeActions() { + when(mConstants.getMinNumSystemGeneratedReplies()).thenReturn(2); + + // Add 2 replies when the minimum is 3 -> we should end up with 0 replies. + String[] choices = new String[] {"This is a very long two-line reply."}; + String[] actions = new String[] {"Short action"}; + + // The action should be displayed on one line only - since it fits! + ViewGroup expectedView = buildExpectedView(new String[] {}, 1 /* lineCount */, + createActions(new String[] {"Short action"})); + expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + + setSmartRepliesAndActions(choices, actions, true /* fromAssistant */); + mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); + + assertEqualMeasures(expectedView, mView); + // smart replies + assertReplyButtonHidden(mView.getChildAt(0)); + // smart actions + assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(1)); + } }