• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.systemui.statusbar.policy;
2 
3 import android.annotation.ColorInt;
4 import android.annotation.NonNull;
5 import android.app.Notification;
6 import android.app.PendingIntent;
7 import android.app.RemoteInput;
8 import android.content.Context;
9 import android.content.res.ColorStateList;
10 import android.content.res.TypedArray;
11 import android.graphics.Canvas;
12 import android.graphics.Color;
13 import android.graphics.drawable.Drawable;
14 import android.graphics.drawable.GradientDrawable;
15 import android.graphics.drawable.InsetDrawable;
16 import android.graphics.drawable.RippleDrawable;
17 import android.text.Layout;
18 import android.text.TextPaint;
19 import android.text.method.TransformationMethod;
20 import android.util.AttributeSet;
21 import android.util.Log;
22 import android.view.LayoutInflater;
23 import android.view.View;
24 import android.view.ViewGroup;
25 import android.widget.Button;
26 import android.widget.TextView;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.internal.util.ContrastColorUtil;
30 import com.android.systemui.R;
31 import com.android.systemui.statusbar.notification.NotificationUtils;
32 
33 import java.text.BreakIterator;
34 import java.util.ArrayList;
35 import java.util.Comparator;
36 import java.util.List;
37 import java.util.PriorityQueue;
38 
39 /** View which displays smart reply and smart actions buttons in notifications. */
40 public class SmartReplyView extends ViewGroup {
41 
42     private static final String TAG = "SmartReplyView";
43 
44     private static final int MEASURE_SPEC_ANY_LENGTH =
45             MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
46 
47     private static final Comparator<View> DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR =
48             (v1, v2) -> ((v2.getMeasuredWidth() - v2.getPaddingLeft() - v2.getPaddingRight())
49                     - (v1.getMeasuredWidth() - v1.getPaddingLeft() - v1.getPaddingRight()));
50 
51     private static final int SQUEEZE_FAILED = -1;
52 
53     /**
54      * The upper bound for the height of this view in pixels. Notifications are automatically
55      * recreated on density or font size changes so caching this should be fine.
56      */
57     private final int mHeightUpperLimit;
58 
59     /** Spacing to be applied between views. */
60     private final int mSpacing;
61 
62     private final BreakIterator mBreakIterator;
63 
64     private PriorityQueue<Button> mCandidateButtonQueueForSqueezing;
65 
66     private View mSmartReplyContainer;
67 
68     /**
69      * Whether the smart replies in this view were generated by the notification assistant. If not
70      * they're provided by the app.
71      */
72     private boolean mSmartRepliesGeneratedByAssistant = false;
73 
74     @ColorInt private int mCurrentBackgroundColor;
75     @ColorInt private final int mDefaultBackgroundColor;
76     @ColorInt private final int mDefaultStrokeColor;
77     @ColorInt private final int mDefaultTextColor;
78     @ColorInt private final int mDefaultTextColorDarkBg;
79     @ColorInt private final int mRippleColorDarkBg;
80     @ColorInt private final int mRippleColor;
81     private final int mStrokeWidth;
82     private final double mMinStrokeContrast;
83 
84     @ColorInt private int mCurrentStrokeColor;
85     @ColorInt private int mCurrentTextColor;
86     @ColorInt private int mCurrentRippleColor;
87     private boolean mCurrentColorized;
88     private int mMaxSqueezeRemeasureAttempts;
89     private int mMaxNumActions;
90     private int mMinNumSystemGeneratedReplies;
91 
SmartReplyView(Context context, AttributeSet attrs)92     public SmartReplyView(Context context, AttributeSet attrs) {
93         super(context, attrs);
94 
95         mHeightUpperLimit = NotificationUtils.getFontScaledHeight(mContext,
96             R.dimen.smart_reply_button_max_height);
97 
98         mDefaultBackgroundColor = context.getColor(R.color.smart_reply_button_background);
99         mDefaultTextColor = mContext.getColor(R.color.smart_reply_button_text);
100         mDefaultTextColorDarkBg = mContext.getColor(R.color.smart_reply_button_text_dark_bg);
101         mDefaultStrokeColor = mContext.getColor(R.color.smart_reply_button_stroke);
102         mRippleColor = mContext.getColor(R.color.notification_ripple_untinted_color);
103         mRippleColorDarkBg = Color.argb(Color.alpha(mRippleColor),
104                 255 /* red */, 255 /* green */, 255 /* blue */);
105         mMinStrokeContrast = ContrastColorUtil.calculateContrast(mDefaultStrokeColor,
106                 mDefaultBackgroundColor);
107 
108         int spacing = 0;
109         int strokeWidth = 0;
110 
111         final TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.SmartReplyView,
112                 0, 0);
113         final int length = arr.getIndexCount();
114         for (int i = 0; i < length; i++) {
115             int attr = arr.getIndex(i);
116             if (attr == R.styleable.SmartReplyView_spacing) {
117                 spacing = arr.getDimensionPixelSize(i, 0);
118             } else if (attr == R.styleable.SmartReplyView_buttonStrokeWidth) {
119                 strokeWidth = arr.getDimensionPixelSize(i, 0);
120             }
121         }
122         arr.recycle();
123 
124         mStrokeWidth = strokeWidth;
125         mSpacing = spacing;
126 
127         mBreakIterator = BreakIterator.getLineInstance();
128 
129         setBackgroundTintColor(mDefaultBackgroundColor, false /* colorized */);
130         reallocateCandidateButtonQueueForSqueezing();
131     }
132 
133     /**
134      * Inflate an instance of this class.
135      */
inflate(Context context, SmartReplyConstants constants)136     public static SmartReplyView inflate(Context context, SmartReplyConstants constants) {
137         SmartReplyView view = (SmartReplyView) LayoutInflater.from(context).inflate(
138                 R.layout.smart_reply_view, null /* root */);
139         view.setMaxNumActions(constants.getMaxNumActions());
140         view.setMaxSqueezeRemeasureAttempts(constants.getMaxSqueezeRemeasureAttempts());
141         view.setMinNumSystemGeneratedReplies(constants.getMinNumSystemGeneratedReplies());
142         return view;
143     }
144 
145     /**
146      * Returns an upper bound for the height of this view in pixels. This method is intended to be
147      * invoked before onMeasure, so it doesn't do any analysis on the contents of the buttons.
148      */
getHeightUpperLimit()149     public int getHeightUpperLimit() {
150        return mHeightUpperLimit;
151     }
152 
reallocateCandidateButtonQueueForSqueezing()153     private void reallocateCandidateButtonQueueForSqueezing() {
154         // Instead of clearing the priority queue, we re-allocate so that it would fit all buttons
155         // exactly. This avoids (1) wasting memory because PriorityQueue never shrinks and
156         // (2) growing in onMeasure.
157         // The constructor throws an IllegalArgument exception if initial capacity is less than 1.
158         mCandidateButtonQueueForSqueezing = new PriorityQueue<>(
159                 Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
160     }
161 
162     /**
163      * Reset the smart suggestions view to allow adding new replies and actions.
164      */
resetSmartSuggestions(View newSmartReplyContainer)165     public void resetSmartSuggestions(View newSmartReplyContainer) {
166         mSmartReplyContainer = newSmartReplyContainer;
167         removeAllViews();
168         setBackgroundTintColor(mDefaultBackgroundColor, false /* colorized */);
169     }
170 
171     /** Add buttons to the {@link SmartReplyView} */
addPreInflatedButtons(List<Button> smartSuggestionButtons)172     public void addPreInflatedButtons(List<Button> smartSuggestionButtons) {
173         for (Button button : smartSuggestionButtons) {
174             addView(button);
175             setButtonColors(button);
176         }
177         reallocateCandidateButtonQueueForSqueezing();
178     }
179 
setMaxNumActions(int maxNumActions)180     public void setMaxNumActions(int maxNumActions) {
181         mMaxNumActions = maxNumActions;
182     }
183 
setMinNumSystemGeneratedReplies(int minNumSystemGeneratedReplies)184     public void setMinNumSystemGeneratedReplies(int minNumSystemGeneratedReplies) {
185         mMinNumSystemGeneratedReplies = minNumSystemGeneratedReplies;
186     }
187 
setMaxSqueezeRemeasureAttempts(int maxSqueezeRemeasureAttempts)188     public void setMaxSqueezeRemeasureAttempts(int maxSqueezeRemeasureAttempts) {
189         mMaxSqueezeRemeasureAttempts = maxSqueezeRemeasureAttempts;
190     }
191 
192     @Override
generateLayoutParams(AttributeSet attrs)193     public LayoutParams generateLayoutParams(AttributeSet attrs) {
194         return new LayoutParams(mContext, attrs);
195     }
196 
197     @Override
generateDefaultLayoutParams()198     protected LayoutParams generateDefaultLayoutParams() {
199         return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
200     }
201 
202     @Override
generateLayoutParams(ViewGroup.LayoutParams params)203     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
204         return new LayoutParams(params.width, params.height);
205     }
206 
clearLayoutLineCount(View view)207     private void clearLayoutLineCount(View view) {
208         if (view instanceof TextView) {
209             ((TextView) view).nullLayouts();
210         }
211     }
212 
213     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)214     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
215         final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
216                 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
217 
218         // Mark all buttons as hidden and un-squeezed.
219         resetButtonsLayoutParams();
220 
221         if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
222             Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
223             mCandidateButtonQueueForSqueezing.clear();
224         }
225 
226         SmartSuggestionMeasures accumulatedMeasures = new SmartSuggestionMeasures(
227                 mPaddingLeft + mPaddingRight,
228                 0 /* maxChildHeight */);
229         int displayedChildCount = 0;
230 
231         // Set up a list of suggestions where actions come before replies. Note that the Buttons
232         // themselves have already been added to the view hierarchy in an order such that Smart
233         // Replies are shown before Smart Actions. The order of the list below determines which
234         // suggestions will be shown at all - only the first X elements are shown (where X depends
235         // on how much space each suggestion button needs).
236         List<View> smartActions = filterActionsOrReplies(SmartButtonType.ACTION);
237         List<View> smartReplies = filterActionsOrReplies(SmartButtonType.REPLY);
238         List<View> smartSuggestions = new ArrayList<>(smartActions);
239         smartSuggestions.addAll(smartReplies);
240         List<View> coveredSuggestions = new ArrayList<>();
241 
242         // SmartSuggestionMeasures for all action buttons, this will be filled in when the first
243         // reply button is added.
244         SmartSuggestionMeasures actionsMeasures = null;
245 
246         final int maxNumActions = mMaxNumActions;
247         int numShownActions = 0;
248 
249         for (View child : smartSuggestions) {
250             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
251             if (maxNumActions != -1 // -1 means 'no limit'
252                     && lp.mButtonType == SmartButtonType.ACTION
253                     && numShownActions >= maxNumActions) {
254                 // We've reached the maximum number of actions, don't add another one!
255                 continue;
256             }
257 
258             clearLayoutLineCount(child);
259             child.measure(MEASURE_SPEC_ANY_LENGTH, heightMeasureSpec);
260 
261             coveredSuggestions.add(child);
262 
263             final int lineCount = ((Button) child).getLineCount();
264             if (lineCount < 1 || lineCount > 2) {
265                 // If smart reply has no text, or more than two lines, then don't show it.
266                 continue;
267             }
268 
269             if (lineCount == 1) {
270                 mCandidateButtonQueueForSqueezing.add((Button) child);
271             }
272 
273             // Remember the current measurements in case the current button doesn't fit in.
274             SmartSuggestionMeasures originalMeasures = accumulatedMeasures.clone();
275             if (actionsMeasures == null && lp.mButtonType == SmartButtonType.REPLY) {
276                 // We've added all actions (we go through actions first), now add their
277                 // measurements.
278                 actionsMeasures = accumulatedMeasures.clone();
279             }
280 
281             final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
282             final int childWidth = child.getMeasuredWidth();
283             final int childHeight = child.getMeasuredHeight();
284             accumulatedMeasures.mMeasuredWidth += spacing + childWidth;
285             accumulatedMeasures.mMaxChildHeight =
286                     Math.max(accumulatedMeasures.mMaxChildHeight, childHeight);
287 
288             // If the last button doesn't fit into the remaining width, try squeezing preceding
289             // smart reply buttons.
290             if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
291                 // Keep squeezing preceding and current smart reply buttons until they all fit.
292                 while (accumulatedMeasures.mMeasuredWidth > targetWidth
293                         && !mCandidateButtonQueueForSqueezing.isEmpty()) {
294                     final Button candidate = mCandidateButtonQueueForSqueezing.poll();
295                     final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
296                     if (squeezeReduction != SQUEEZE_FAILED) {
297                         accumulatedMeasures.mMaxChildHeight =
298                                 Math.max(accumulatedMeasures.mMaxChildHeight,
299                                         candidate.getMeasuredHeight());
300                         accumulatedMeasures.mMeasuredWidth -= squeezeReduction;
301                     }
302                 }
303 
304                 // If the current button still doesn't fit after squeezing all buttons, undo the
305                 // last squeezing round.
306                 if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
307                     accumulatedMeasures = originalMeasures;
308 
309                     // Mark all buttons from the last squeezing round as "failed to squeeze", so
310                     // that they're re-measured without squeezing later.
311                     markButtonsWithPendingSqueezeStatusAs(
312                             LayoutParams.SQUEEZE_STATUS_FAILED, coveredSuggestions);
313 
314                     // The current button doesn't fit, keep on adding lower-priority buttons in case
315                     // any of those fit.
316                     continue;
317                 }
318 
319                 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
320                 // to prevent them from being un-squeezed in a subsequent squeezing round.
321                 markButtonsWithPendingSqueezeStatusAs(
322                         LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, coveredSuggestions);
323             }
324 
325             lp.show = true;
326             displayedChildCount++;
327             if (lp.mButtonType == SmartButtonType.ACTION) {
328                 numShownActions++;
329             }
330         }
331 
332         if (mSmartRepliesGeneratedByAssistant) {
333             if (!gotEnoughSmartReplies(smartReplies)) {
334                 // We don't have enough smart replies - hide all of them.
335                 for (View smartReplyButton : smartReplies) {
336                     final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
337                     lp.show = false;
338                 }
339                 // Reset our measures back to when we had only added actions (before adding
340                 // replies).
341                 accumulatedMeasures = actionsMeasures;
342             }
343         }
344 
345         // We're done squeezing buttons, so we can clear the priority queue.
346         mCandidateButtonQueueForSqueezing.clear();
347 
348         // Finally, we need to re-measure some buttons.
349         remeasureButtonsIfNecessary(accumulatedMeasures.mMaxChildHeight);
350 
351         int buttonHeight = Math.max(getSuggestedMinimumHeight(), mPaddingTop
352                 + accumulatedMeasures.mMaxChildHeight + mPaddingBottom);
353 
354         setMeasuredDimension(
355                 resolveSize(Math.max(getSuggestedMinimumWidth(),
356                                      accumulatedMeasures.mMeasuredWidth),
357                             widthMeasureSpec),
358                 resolveSize(buttonHeight, heightMeasureSpec));
359     }
360 
361     // TODO: this should be replaced, and instead, setMinSystemGenerated... should be invoked
362     //  with MAX_VALUE if mSmartRepliesGeneratedByAssistant would be false (essentially, this is a
363     //  ViewModel decision, as opposed to a View decision)
setSmartRepliesGeneratedByAssistant(boolean fromAssistant)364     void setSmartRepliesGeneratedByAssistant(boolean fromAssistant) {
365         mSmartRepliesGeneratedByAssistant = fromAssistant;
366     }
367 
hideSmartSuggestions()368     void hideSmartSuggestions() {
369         if (mSmartReplyContainer != null) {
370             mSmartReplyContainer.setVisibility(View.GONE);
371         }
372     }
373 
374     /**
375      * Fields we keep track of inside onMeasure() to correctly measure the SmartReplyView depending
376      * on which suggestions are added.
377      */
378     private static class SmartSuggestionMeasures {
379         int mMeasuredWidth = -1;
380         int mMaxChildHeight = -1;
381 
SmartSuggestionMeasures(int measuredWidth, int maxChildHeight)382         SmartSuggestionMeasures(int measuredWidth, int maxChildHeight) {
383             this.mMeasuredWidth = measuredWidth;
384             this.mMaxChildHeight = maxChildHeight;
385         }
386 
clone()387         public SmartSuggestionMeasures clone() {
388             return new SmartSuggestionMeasures(mMeasuredWidth, mMaxChildHeight);
389         }
390     }
391 
392     /**
393      * Returns whether our notification contains at least N smart replies (or 0) where N is
394      * determined by {@link SmartReplyConstants}.
395      */
396     // TODO: we probably sholdn't make this deliberation in the View
gotEnoughSmartReplies(List<View> smartReplies)397     private boolean gotEnoughSmartReplies(List<View> smartReplies) {
398         int numShownReplies = 0;
399         for (View smartReplyButton : smartReplies) {
400             final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
401             if (lp.show) {
402                 numShownReplies++;
403             }
404         }
405         if (numShownReplies == 0 || numShownReplies >= mMinNumSystemGeneratedReplies) {
406             // We have enough replies, yay!
407             return true;
408         }
409         return false;
410     }
411 
filterActionsOrReplies(SmartButtonType buttonType)412     private List<View> filterActionsOrReplies(SmartButtonType buttonType) {
413         List<View> actions = new ArrayList<>();
414         final int childCount = getChildCount();
415         for (int i = 0; i < childCount; i++) {
416             final View child = getChildAt(i);
417             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
418             if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
419                 continue;
420             }
421             if (lp.mButtonType == buttonType) {
422                 actions.add(child);
423             }
424         }
425         return actions;
426     }
427 
resetButtonsLayoutParams()428     private void resetButtonsLayoutParams() {
429         final int childCount = getChildCount();
430         for (int i = 0; i < childCount; i++) {
431             final View child = getChildAt(i);
432             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
433             lp.show = false;
434             lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
435         }
436     }
437 
squeezeButton(Button button, int heightMeasureSpec)438     private int squeezeButton(Button button, int heightMeasureSpec) {
439         final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
440         if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
441             return SQUEEZE_FAILED;
442         }
443         return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
444     }
445 
estimateOptimalSqueezedButtonTextWidth(Button button)446     private int estimateOptimalSqueezedButtonTextWidth(Button button) {
447         // Find a line-break point in the middle of the smart reply button text.
448         final String rawText = button.getText().toString();
449 
450         // The button sometimes has a transformation affecting text layout (e.g. all caps).
451         final TransformationMethod transformation = button.getTransformationMethod();
452         final String text = transformation == null ?
453                 rawText : transformation.getTransformation(rawText, button).toString();
454         final int length = text.length();
455         mBreakIterator.setText(text);
456 
457         if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
458             if (mBreakIterator.next() == BreakIterator.DONE) {
459                 // Can't find a single possible line break in either direction.
460                 return SQUEEZE_FAILED;
461             }
462         }
463 
464         final TextPaint paint = button.getPaint();
465         final int initialPosition = mBreakIterator.current();
466         final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
467         final float initialRightTextWidth =
468                 Layout.getDesiredWidth(text, initialPosition, length, paint);
469         float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
470 
471         if (initialLeftTextWidth != initialRightTextWidth) {
472             // See if there's a better line-break point (leading to a more narrow button) in
473             // either left or right direction.
474             final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
475             final int maxSqueezeRemeasureAttempts = mMaxSqueezeRemeasureAttempts;
476             for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
477                 final int newPosition =
478                         moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
479                 if (newPosition == BreakIterator.DONE) {
480                     break;
481                 }
482 
483                 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
484                 final float newRightTextWidth =
485                         Layout.getDesiredWidth(text, newPosition, length, paint);
486                 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
487                 if (newOptimalTextWidth < optimalTextWidth) {
488                     optimalTextWidth = newOptimalTextWidth;
489                 } else {
490                     break;
491                 }
492 
493                 boolean tooFar = moveLeft
494                         ? newLeftTextWidth <= newRightTextWidth
495                         : newLeftTextWidth >= newRightTextWidth;
496                 if (tooFar) {
497                     break;
498                 }
499             }
500         }
501 
502         return (int) Math.ceil(optimalTextWidth);
503     }
504 
505     /**
506      * Returns the combined width of the left drawable (the action icon) and the padding between the
507      * drawable and the button text.
508      */
getLeftCompoundDrawableWidthWithPadding(Button button)509     private int getLeftCompoundDrawableWidthWithPadding(Button button) {
510         Drawable[] drawables = button.getCompoundDrawables();
511         Drawable leftDrawable = drawables[0];
512         if (leftDrawable == null) return 0;
513 
514         return leftDrawable.getBounds().width() + button.getCompoundDrawablePadding();
515     }
516 
squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth)517     private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
518         int oldWidth = button.getMeasuredWidth();
519 
520         // Re-measure the squeezed smart reply button.
521         clearLayoutLineCount(button);
522         final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
523                 button.getPaddingLeft() + button.getPaddingRight() + textWidth
524                       + getLeftCompoundDrawableWidthWithPadding(button), MeasureSpec.AT_MOST);
525         button.measure(widthMeasureSpec, heightMeasureSpec);
526 
527         final int newWidth = button.getMeasuredWidth();
528 
529         final LayoutParams lp = (LayoutParams) button.getLayoutParams();
530         if (button.getLineCount() > 2 || newWidth >= oldWidth) {
531             lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
532             return SQUEEZE_FAILED;
533         } else {
534             lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
535             return oldWidth - newWidth;
536         }
537     }
538 
remeasureButtonsIfNecessary(int maxChildHeight)539     private void remeasureButtonsIfNecessary(int maxChildHeight) {
540         final int maxChildHeightMeasure =
541                 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
542 
543         final int childCount = getChildCount();
544         for (int i = 0; i < childCount; i++) {
545             final View child = getChildAt(i);
546             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
547             if (!lp.show) {
548                 continue;
549             }
550 
551             boolean requiresNewMeasure = false;
552             int newWidth = child.getMeasuredWidth();
553 
554             // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
555             // in more than two lines or because it was unnecessary).
556             if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
557                 requiresNewMeasure = true;
558                 newWidth = Integer.MAX_VALUE;
559             }
560 
561             // Re-measure reason 2: The button's height is less than the max height of all buttons
562             // (all should have the same height).
563             if (child.getMeasuredHeight() != maxChildHeight) {
564                 requiresNewMeasure = true;
565             }
566 
567             if (requiresNewMeasure) {
568                 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
569                         maxChildHeightMeasure);
570             }
571         }
572     }
573 
markButtonsWithPendingSqueezeStatusAs( int squeezeStatus, List<View> coveredChildren)574     private void markButtonsWithPendingSqueezeStatusAs(
575             int squeezeStatus, List<View> coveredChildren) {
576         for (View child : coveredChildren) {
577             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
578             if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
579                 lp.squeezeStatus = squeezeStatus;
580             }
581         }
582     }
583 
584     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)585     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
586         final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
587 
588         final int width = right - left;
589         int position = isRtl ? width - mPaddingRight : mPaddingLeft;
590 
591         final int childCount = getChildCount();
592         for (int i = 0; i < childCount; i++) {
593             final View child = getChildAt(i);
594             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
595             if (!lp.show) {
596                 continue;
597             }
598 
599             final int childWidth = child.getMeasuredWidth();
600             final int childHeight = child.getMeasuredHeight();
601             final int childLeft = isRtl ? position - childWidth : position;
602             child.layout(childLeft, 0, childLeft + childWidth, childHeight);
603 
604             final int childWidthWithSpacing = childWidth + mSpacing;
605             if (isRtl) {
606                 position -= childWidthWithSpacing;
607             } else {
608                 position += childWidthWithSpacing;
609             }
610         }
611     }
612 
613     @Override
drawChild(Canvas canvas, View child, long drawingTime)614     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
615         final LayoutParams lp = (LayoutParams) child.getLayoutParams();
616         return lp.show && super.drawChild(canvas, child, drawingTime);
617     }
618 
619     /**
620      * Set the current background color of the notification so that the smart reply buttons can
621      * match it, and calculate other colors (e.g. text, ripple, stroke)
622      */
setBackgroundTintColor(int backgroundColor, boolean colorized)623     public void setBackgroundTintColor(int backgroundColor, boolean colorized) {
624         if (backgroundColor == mCurrentBackgroundColor && colorized == mCurrentColorized) {
625             // Same color ignoring.
626            return;
627         }
628         mCurrentBackgroundColor = backgroundColor;
629         mCurrentColorized = colorized;
630 
631         final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor);
632 
633         mCurrentTextColor = ContrastColorUtil.ensureTextContrast(
634                 dark ? mDefaultTextColorDarkBg : mDefaultTextColor,
635                 backgroundColor | 0xff000000, dark);
636         mCurrentStrokeColor = colorized ? mCurrentTextColor : ContrastColorUtil.ensureContrast(
637                 mDefaultStrokeColor, backgroundColor | 0xff000000, dark, mMinStrokeContrast);
638         mCurrentRippleColor = dark ? mRippleColorDarkBg : mRippleColor;
639 
640         int childCount = getChildCount();
641         for (int i = 0; i < childCount; i++) {
642             setButtonColors((Button) getChildAt(i));
643         }
644     }
645 
setButtonColors(Button button)646     private void setButtonColors(Button button) {
647         Drawable drawable = button.getBackground();
648         if (drawable instanceof RippleDrawable) {
649             // Mutate in case other notifications are using this drawable.
650             drawable = drawable.mutate();
651             RippleDrawable ripple = (RippleDrawable) drawable;
652             ripple.setColor(ColorStateList.valueOf(mCurrentRippleColor));
653             Drawable inset = ripple.getDrawable(0);
654             if (inset instanceof InsetDrawable) {
655                 Drawable background = ((InsetDrawable) inset).getDrawable();
656                 if (background instanceof GradientDrawable) {
657                     GradientDrawable gradientDrawable = (GradientDrawable) background;
658                     gradientDrawable.setColor(mCurrentBackgroundColor);
659                     gradientDrawable.setStroke(mStrokeWidth, mCurrentStrokeColor);
660                 }
661             }
662             button.setBackground(drawable);
663         }
664         button.setTextColor(mCurrentTextColor);
665     }
666 
667     enum SmartButtonType {
668         REPLY,
669         ACTION
670     }
671 
672     @VisibleForTesting
673     static class LayoutParams extends ViewGroup.LayoutParams {
674 
675         /** Button is not squeezed. */
676         private static final int SQUEEZE_STATUS_NONE = 0;
677 
678         /**
679          * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
680          * turns out to have been unnecessary (because there's still not enough space to add another
681          * button).
682          */
683         private static final int SQUEEZE_STATUS_PENDING = 1;
684 
685         /** Button was successfully squeezed and it won't be un-squeezed. */
686         private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
687 
688         /**
689          * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
690          * text or it didn't reduce the button's width at all. The button will have to be
691          * re-measured to use only one line of text.
692          */
693         private static final int SQUEEZE_STATUS_FAILED = 3;
694 
695         private boolean show = false;
696         private int squeezeStatus = SQUEEZE_STATUS_NONE;
697         SmartButtonType mButtonType = SmartButtonType.REPLY;
698 
LayoutParams(Context c, AttributeSet attrs)699         private LayoutParams(Context c, AttributeSet attrs) {
700             super(c, attrs);
701         }
702 
LayoutParams(int width, int height)703         private LayoutParams(int width, int height) {
704             super(width, height);
705         }
706 
707         @VisibleForTesting
isShown()708         boolean isShown() {
709             return show;
710         }
711     }
712 
713     /**
714      * Data class for smart replies.
715      */
716     public static class SmartReplies {
717         @NonNull
718         public final RemoteInput remoteInput;
719         @NonNull
720         public final PendingIntent pendingIntent;
721         @NonNull
722         public final List<CharSequence> choices;
723         public final boolean fromAssistant;
724 
SmartReplies(@onNull List<CharSequence> choices, @NonNull RemoteInput remoteInput, @NonNull PendingIntent pendingIntent, boolean fromAssistant)725         public SmartReplies(@NonNull List<CharSequence> choices, @NonNull RemoteInput remoteInput,
726                 @NonNull PendingIntent pendingIntent, boolean fromAssistant) {
727             this.choices = choices;
728             this.remoteInput = remoteInput;
729             this.pendingIntent = pendingIntent;
730             this.fromAssistant = fromAssistant;
731         }
732     }
733 
734 
735     /**
736      * Data class for smart actions.
737      */
738     public static class SmartActions {
739         @NonNull
740         public final List<Notification.Action> actions;
741         public final boolean fromAssistant;
742 
SmartActions(@onNull List<Notification.Action> actions, boolean fromAssistant)743         public SmartActions(@NonNull List<Notification.Action> actions, boolean fromAssistant) {
744             this.actions = actions;
745             this.fromAssistant = fromAssistant;
746         }
747     }
748 }
749