1 package com.android.systemui.statusbar.policy;
2 
3 import android.annotation.ColorInt;
4 import android.app.PendingIntent;
5 import android.app.RemoteInput;
6 import android.content.Context;
7 import android.content.Intent;
8 import android.content.res.ColorStateList;
9 import android.content.res.TypedArray;
10 import android.graphics.Canvas;
11 import android.graphics.Color;
12 import android.graphics.drawable.Drawable;
13 import android.graphics.drawable.GradientDrawable;
14 import android.graphics.drawable.InsetDrawable;
15 import android.graphics.drawable.RippleDrawable;
16 import android.os.Bundle;
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.view.accessibility.AccessibilityNodeInfo;
26 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
27 import android.widget.Button;
28 
29 import com.android.internal.annotations.VisibleForTesting;
30 import com.android.internal.util.NotificationColorUtil;
31 import com.android.keyguard.KeyguardHostView.OnDismissAction;
32 import com.android.systemui.Dependency;
33 import com.android.systemui.R;
34 import com.android.systemui.statusbar.NotificationData;
35 import com.android.systemui.statusbar.SmartReplyController;
36 import com.android.systemui.statusbar.notification.NotificationUtils;
37 import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
38 
39 import java.text.BreakIterator;
40 import java.util.Comparator;
41 import java.util.PriorityQueue;
42 
43 /** View which displays smart reply buttons in notifications. */
44 public class SmartReplyView extends ViewGroup {
45 
46     private static final String TAG = "SmartReplyView";
47 
48     private static final int MEASURE_SPEC_ANY_WIDTH =
49             MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
50 
51     private static final Comparator<View> DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR =
52             (v1, v2) -> ((v2.getMeasuredWidth() - v2.getPaddingLeft() - v2.getPaddingRight())
53                     - (v1.getMeasuredWidth() - v1.getPaddingLeft() - v1.getPaddingRight()));
54 
55     private static final int SQUEEZE_FAILED = -1;
56 
57     private final SmartReplyConstants mConstants;
58     private final KeyguardDismissUtil mKeyguardDismissUtil;
59 
60     /**
61      * The upper bound for the height of this view in pixels. Notifications are automatically
62      * recreated on density or font size changes so caching this should be fine.
63      */
64     private final int mHeightUpperLimit;
65 
66     /** Spacing to be applied between views. */
67     private final int mSpacing;
68 
69     /** Horizontal padding of smart reply buttons if all of them use only one line of text. */
70     private final int mSingleLineButtonPaddingHorizontal;
71 
72     /** Horizontal padding of smart reply buttons if at least one of them uses two lines of text. */
73     private final int mDoubleLineButtonPaddingHorizontal;
74 
75     /** Increase in width of a smart reply button as a result of using two lines instead of one. */
76     private final int mSingleToDoubleLineButtonWidthIncrease;
77 
78     private final BreakIterator mBreakIterator;
79 
80     private PriorityQueue<Button> mCandidateButtonQueueForSqueezing;
81 
82     private View mSmartReplyContainer;
83 
84     @ColorInt
85     private int mCurrentBackgroundColor;
86     @ColorInt
87     private final int mDefaultBackgroundColor;
88     @ColorInt
89     private final int mDefaultStrokeColor;
90     @ColorInt
91     private final int mDefaultTextColor;
92     @ColorInt
93     private final int mDefaultTextColorDarkBg;
94     @ColorInt
95     private final int mRippleColorDarkBg;
96     @ColorInt
97     private final int mRippleColor;
98     private final int mStrokeWidth;
99     private final double mMinStrokeContrast;
100 
SmartReplyView(Context context, AttributeSet attrs)101     public SmartReplyView(Context context, AttributeSet attrs) {
102         super(context, attrs);
103         mConstants = Dependency.get(SmartReplyConstants.class);
104         mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class);
105 
106         mHeightUpperLimit = NotificationUtils.getFontScaledHeight(mContext,
107             R.dimen.smart_reply_button_max_height);
108 
109         mCurrentBackgroundColor = context.getColor(R.color.smart_reply_button_background);
110         mDefaultBackgroundColor = mCurrentBackgroundColor;
111         mDefaultTextColor = mContext.getColor(R.color.smart_reply_button_text);
112         mDefaultTextColorDarkBg = mContext.getColor(R.color.smart_reply_button_text_dark_bg);
113         mDefaultStrokeColor = mContext.getColor(R.color.smart_reply_button_stroke);
114         mRippleColor = mContext.getColor(R.color.notification_ripple_untinted_color);
115         mRippleColorDarkBg = Color.argb(Color.alpha(mRippleColor),
116                 255 /* red */, 255 /* green */, 255 /* blue */);
117         mMinStrokeContrast = NotificationColorUtil.calculateContrast(mDefaultStrokeColor,
118                 mDefaultBackgroundColor);
119 
120         int spacing = 0;
121         int singleLineButtonPaddingHorizontal = 0;
122         int doubleLineButtonPaddingHorizontal = 0;
123         int strokeWidth = 0;
124 
125         final TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.SmartReplyView,
126                 0, 0);
127         final int length = arr.getIndexCount();
128         for (int i = 0; i < length; i++) {
129             int attr = arr.getIndex(i);
130             switch (attr) {
131                 case R.styleable.SmartReplyView_spacing:
132                     spacing = arr.getDimensionPixelSize(i, 0);
133                     break;
134                 case R.styleable.SmartReplyView_singleLineButtonPaddingHorizontal:
135                     singleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
136                     break;
137                 case R.styleable.SmartReplyView_doubleLineButtonPaddingHorizontal:
138                     doubleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
139                     break;
140                 case R.styleable.SmartReplyView_buttonStrokeWidth:
141                     strokeWidth = arr.getDimensionPixelSize(i, 0);
142                     break;
143             }
144         }
145         arr.recycle();
146 
147         mStrokeWidth = strokeWidth;
148         mSpacing = spacing;
149         mSingleLineButtonPaddingHorizontal = singleLineButtonPaddingHorizontal;
150         mDoubleLineButtonPaddingHorizontal = doubleLineButtonPaddingHorizontal;
151         mSingleToDoubleLineButtonWidthIncrease =
152                 2 * (doubleLineButtonPaddingHorizontal - singleLineButtonPaddingHorizontal);
153 
154 
155         mBreakIterator = BreakIterator.getLineInstance();
156         reallocateCandidateButtonQueueForSqueezing();
157     }
158 
159     /**
160      * Returns an upper bound for the height of this view in pixels. This method is intended to be
161      * invoked before onMeasure, so it doesn't do any analysis on the contents of the buttons.
162      */
getHeightUpperLimit()163     public int getHeightUpperLimit() {
164        return mHeightUpperLimit;
165     }
166 
reallocateCandidateButtonQueueForSqueezing()167     private void reallocateCandidateButtonQueueForSqueezing() {
168         // Instead of clearing the priority queue, we re-allocate so that it would fit all buttons
169         // exactly. This avoids (1) wasting memory because PriorityQueue never shrinks and
170         // (2) growing in onMeasure.
171         // The constructor throws an IllegalArgument exception if initial capacity is less than 1.
172         mCandidateButtonQueueForSqueezing = new PriorityQueue<>(
173                 Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
174     }
175 
setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent, SmartReplyController smartReplyController, NotificationData.Entry entry, View smartReplyContainer)176     public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent,
177             SmartReplyController smartReplyController, NotificationData.Entry entry,
178             View smartReplyContainer) {
179         mSmartReplyContainer = smartReplyContainer;
180         removeAllViews();
181         mCurrentBackgroundColor = mDefaultBackgroundColor;
182         if (remoteInput != null && pendingIntent != null) {
183             CharSequence[] choices = remoteInput.getChoices();
184             if (choices != null) {
185                 for (int i = 0; i < choices.length; ++i) {
186                     Button replyButton = inflateReplyButton(
187                             getContext(), this, i, choices[i], remoteInput, pendingIntent,
188                             smartReplyController, entry);
189                     addView(replyButton);
190                 }
191             }
192         }
193         reallocateCandidateButtonQueueForSqueezing();
194     }
195 
inflate(Context context, ViewGroup root)196     public static SmartReplyView inflate(Context context, ViewGroup root) {
197         return (SmartReplyView)
198                 LayoutInflater.from(context).inflate(R.layout.smart_reply_view, root, false);
199     }
200 
201     @VisibleForTesting
inflateReplyButton(Context context, ViewGroup root, int replyIndex, CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent, SmartReplyController smartReplyController, NotificationData.Entry entry)202     Button inflateReplyButton(Context context, ViewGroup root, int replyIndex,
203             CharSequence choice, RemoteInput remoteInput, PendingIntent pendingIntent,
204             SmartReplyController smartReplyController, NotificationData.Entry entry) {
205         Button b = (Button) LayoutInflater.from(context).inflate(
206                 R.layout.smart_reply_button, root, false);
207         b.setText(choice);
208 
209         OnDismissAction action = () -> {
210             smartReplyController.smartReplySent(entry, replyIndex, b.getText());
211             Bundle results = new Bundle();
212             results.putString(remoteInput.getResultKey(), choice.toString());
213             Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
214             RemoteInput.addResultsToIntent(new RemoteInput[]{remoteInput}, intent, results);
215             RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
216             entry.setHasSentReply();
217             try {
218                 pendingIntent.send(context, 0, intent);
219             } catch (PendingIntent.CanceledException e) {
220                 Log.w(TAG, "Unable to send smart reply", e);
221             }
222             mSmartReplyContainer.setVisibility(View.GONE);
223             return false; // do not defer
224         };
225 
226         b.setOnClickListener(view -> {
227             mKeyguardDismissUtil.executeWhenUnlocked(action);
228         });
229 
230         b.setAccessibilityDelegate(new AccessibilityDelegate() {
231             public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
232                 super.onInitializeAccessibilityNodeInfo(host, info);
233                 String label = getResources().getString(R.string.accessibility_send_smart_reply);
234                 info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
235             }
236         });
237 
238         setColors(b, mCurrentBackgroundColor, mDefaultStrokeColor, mDefaultTextColor, mRippleColor);
239         return b;
240     }
241 
242     @Override
generateLayoutParams(AttributeSet attrs)243     public LayoutParams generateLayoutParams(AttributeSet attrs) {
244         return new LayoutParams(mContext, attrs);
245     }
246 
247     @Override
generateDefaultLayoutParams()248     protected LayoutParams generateDefaultLayoutParams() {
249         return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
250     }
251 
252     @Override
generateLayoutParams(ViewGroup.LayoutParams params)253     protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
254         return new LayoutParams(params.width, params.height);
255     }
256 
257     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)258     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
259         final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
260                 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
261 
262         // Mark all buttons as hidden and un-squeezed.
263         resetButtonsLayoutParams();
264 
265         if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
266             Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
267             mCandidateButtonQueueForSqueezing.clear();
268         }
269 
270         int measuredWidth = mPaddingLeft + mPaddingRight;
271         int maxChildHeight = 0;
272         int displayedChildCount = 0;
273         int buttonPaddingHorizontal = mSingleLineButtonPaddingHorizontal;
274 
275         final int childCount = getChildCount();
276         for (int i = 0; i < childCount; i++) {
277             final View child = getChildAt(i);
278             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
279             if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
280                 continue;
281             }
282 
283             child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
284                     buttonPaddingHorizontal, child.getPaddingBottom());
285             child.measure(MEASURE_SPEC_ANY_WIDTH, heightMeasureSpec);
286 
287             final int lineCount = ((Button) child).getLineCount();
288             if (lineCount < 1 || lineCount > 2) {
289                 // If smart reply has no text, or more than two lines, then don't show it.
290                 continue;
291             }
292 
293             if (lineCount == 1) {
294                 mCandidateButtonQueueForSqueezing.add((Button) child);
295             }
296 
297             // Remember the current measurements in case the current button doesn't fit in.
298             final int originalMaxChildHeight = maxChildHeight;
299             final int originalMeasuredWidth = measuredWidth;
300             final int originalButtonPaddingHorizontal = buttonPaddingHorizontal;
301 
302             final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
303             final int childWidth = child.getMeasuredWidth();
304             final int childHeight = child.getMeasuredHeight();
305             measuredWidth += spacing + childWidth;
306             maxChildHeight = Math.max(maxChildHeight, childHeight);
307 
308             // Do we need to increase the number of lines in smart reply buttons to two?
309             final boolean increaseToTwoLines =
310                     buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal
311                             && (lineCount == 2 || measuredWidth > targetWidth);
312             if (increaseToTwoLines) {
313                 measuredWidth += (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease;
314                 buttonPaddingHorizontal = mDoubleLineButtonPaddingHorizontal;
315             }
316 
317             // If the last button doesn't fit into the remaining width, try squeezing preceding
318             // smart reply buttons.
319             if (measuredWidth > targetWidth) {
320                 // Keep squeezing preceding and current smart reply buttons until they all fit.
321                 while (measuredWidth > targetWidth
322                         && !mCandidateButtonQueueForSqueezing.isEmpty()) {
323                     final Button candidate = mCandidateButtonQueueForSqueezing.poll();
324                     final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
325                     if (squeezeReduction != SQUEEZE_FAILED) {
326                         maxChildHeight = Math.max(maxChildHeight, candidate.getMeasuredHeight());
327                         measuredWidth -= squeezeReduction;
328                     }
329                 }
330 
331                 // If the current button still doesn't fit after squeezing all buttons, undo the
332                 // last squeezing round.
333                 if (measuredWidth > targetWidth) {
334                     measuredWidth = originalMeasuredWidth;
335                     maxChildHeight = originalMaxChildHeight;
336                     buttonPaddingHorizontal = originalButtonPaddingHorizontal;
337 
338                     // Mark all buttons from the last squeezing round as "failed to squeeze", so
339                     // that they're re-measured without squeezing later.
340                     markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_FAILED, i);
341 
342                     // The current button doesn't fit, so there's no point in measuring further
343                     // buttons.
344                     break;
345                 }
346 
347                 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
348                 // to prevent them from being un-squeezed in a subsequent squeezing round.
349                 markButtonsWithPendingSqueezeStatusAs(LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, i);
350             }
351 
352             lp.show = true;
353             displayedChildCount++;
354         }
355 
356         // We're done squeezing buttons, so we can clear the priority queue.
357         mCandidateButtonQueueForSqueezing.clear();
358 
359         // Finally, we need to re-measure some buttons.
360         remeasureButtonsIfNecessary(buttonPaddingHorizontal, maxChildHeight);
361 
362         setMeasuredDimension(
363                 resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec),
364                 resolveSize(Math.max(getSuggestedMinimumHeight(),
365                         mPaddingTop + maxChildHeight + mPaddingBottom), heightMeasureSpec));
366     }
367 
resetButtonsLayoutParams()368     private void resetButtonsLayoutParams() {
369         final int childCount = getChildCount();
370         for (int i = 0; i < childCount; i++) {
371             final View child = getChildAt(i);
372             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
373             lp.show = false;
374             lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
375         }
376     }
377 
squeezeButton(Button button, int heightMeasureSpec)378     private int squeezeButton(Button button, int heightMeasureSpec) {
379         final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
380         if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
381             return SQUEEZE_FAILED;
382         }
383         return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
384     }
385 
estimateOptimalSqueezedButtonTextWidth(Button button)386     private int estimateOptimalSqueezedButtonTextWidth(Button button) {
387         // Find a line-break point in the middle of the smart reply button text.
388         final String rawText = button.getText().toString();
389 
390         // The button sometimes has a transformation affecting text layout (e.g. all caps).
391         final TransformationMethod transformation = button.getTransformationMethod();
392         final String text = transformation == null ?
393                 rawText : transformation.getTransformation(rawText, button).toString();
394         final int length = text.length();
395         mBreakIterator.setText(text);
396 
397         if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
398             if (mBreakIterator.next() == BreakIterator.DONE) {
399                 // Can't find a single possible line break in either direction.
400                 return SQUEEZE_FAILED;
401             }
402         }
403 
404         final TextPaint paint = button.getPaint();
405         final int initialPosition = mBreakIterator.current();
406         final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
407         final float initialRightTextWidth =
408                 Layout.getDesiredWidth(text, initialPosition, length, paint);
409         float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
410 
411         if (initialLeftTextWidth != initialRightTextWidth) {
412             // See if there's a better line-break point (leading to a more narrow button) in
413             // either left or right direction.
414             final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
415             final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
416             for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
417                 final int newPosition =
418                         moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
419                 if (newPosition == BreakIterator.DONE) {
420                     break;
421                 }
422 
423                 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
424                 final float newRightTextWidth =
425                         Layout.getDesiredWidth(text, newPosition, length, paint);
426                 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
427                 if (newOptimalTextWidth < optimalTextWidth) {
428                     optimalTextWidth = newOptimalTextWidth;
429                 } else {
430                     break;
431                 }
432 
433                 boolean tooFar = moveLeft
434                         ? newLeftTextWidth <= newRightTextWidth
435                         : newLeftTextWidth >= newRightTextWidth;
436                 if (tooFar) {
437                     break;
438                 }
439             }
440         }
441 
442         return (int) Math.ceil(optimalTextWidth);
443     }
444 
squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth)445     private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
446         int oldWidth = button.getMeasuredWidth();
447         if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
448             // Correct for the fact that the button was laid out with single-line horizontal
449             // padding.
450             oldWidth += mSingleToDoubleLineButtonWidthIncrease;
451         }
452 
453         // Re-measure the squeezed smart reply button.
454         button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
455                 mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
456         final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
457                 2 * mDoubleLineButtonPaddingHorizontal + textWidth, MeasureSpec.AT_MOST);
458         button.measure(widthMeasureSpec, heightMeasureSpec);
459 
460         final int newWidth = button.getMeasuredWidth();
461 
462         final LayoutParams lp = (LayoutParams) button.getLayoutParams();
463         if (button.getLineCount() > 2 || newWidth >= oldWidth) {
464             lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
465             return SQUEEZE_FAILED;
466         } else {
467             lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
468             return oldWidth - newWidth;
469         }
470     }
471 
remeasureButtonsIfNecessary( int buttonPaddingHorizontal, int maxChildHeight)472     private void remeasureButtonsIfNecessary(
473             int buttonPaddingHorizontal, int maxChildHeight) {
474         final int maxChildHeightMeasure =
475                 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
476 
477         final int childCount = getChildCount();
478         for (int i = 0; i < childCount; i++) {
479             final View child = getChildAt(i);
480             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
481             if (!lp.show) {
482                 continue;
483             }
484 
485             boolean requiresNewMeasure = false;
486             int newWidth = child.getMeasuredWidth();
487 
488             // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
489             // in more than two lines or because it was unnecessary).
490             if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
491                 requiresNewMeasure = true;
492                 newWidth = Integer.MAX_VALUE;
493             }
494 
495             // Re-measure reason 2: The button's horizontal padding is incorrect (because it was
496             // measured with the wrong number of lines).
497             if (child.getPaddingLeft() != buttonPaddingHorizontal) {
498                 requiresNewMeasure = true;
499                 if (newWidth != Integer.MAX_VALUE) {
500                     if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) {
501                         // Change padding (2->1 line).
502                         newWidth -= mSingleToDoubleLineButtonWidthIncrease;
503                     } else {
504                         // Change padding (1->2 lines).
505                         newWidth += mSingleToDoubleLineButtonWidthIncrease;
506                     }
507                 }
508                 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
509                         buttonPaddingHorizontal, child.getPaddingBottom());
510             }
511 
512             // Re-measure reason 3: The button's height is less than the max height of all buttons
513             // (all should have the same height).
514             if (child.getMeasuredHeight() != maxChildHeight) {
515                 requiresNewMeasure = true;
516             }
517 
518             if (requiresNewMeasure) {
519                 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
520                         maxChildHeightMeasure);
521             }
522         }
523     }
524 
markButtonsWithPendingSqueezeStatusAs(int squeezeStatus, int maxChildIndex)525     private void markButtonsWithPendingSqueezeStatusAs(int squeezeStatus, int maxChildIndex) {
526         for (int i = 0; i <= maxChildIndex; i++) {
527             final View child = getChildAt(i);
528             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
529             if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
530                 lp.squeezeStatus = squeezeStatus;
531             }
532         }
533     }
534 
535     @Override
onLayout(boolean changed, int left, int top, int right, int bottom)536     protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
537         final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
538 
539         final int width = right - left;
540         int position = isRtl ? width - mPaddingRight : mPaddingLeft;
541 
542         final int childCount = getChildCount();
543         for (int i = 0; i < childCount; i++) {
544             final View child = getChildAt(i);
545             final LayoutParams lp = (LayoutParams) child.getLayoutParams();
546             if (!lp.show) {
547                 continue;
548             }
549 
550             final int childWidth = child.getMeasuredWidth();
551             final int childHeight = child.getMeasuredHeight();
552             final int childLeft = isRtl ? position - childWidth : position;
553             child.layout(childLeft, 0, childLeft + childWidth, childHeight);
554 
555             final int childWidthWithSpacing = childWidth + mSpacing;
556             if (isRtl) {
557                 position -= childWidthWithSpacing;
558             } else {
559                 position += childWidthWithSpacing;
560             }
561         }
562     }
563 
564     @Override
drawChild(Canvas canvas, View child, long drawingTime)565     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
566         final LayoutParams lp = (LayoutParams) child.getLayoutParams();
567         return lp.show && super.drawChild(canvas, child, drawingTime);
568     }
569 
setBackgroundTintColor(int backgroundColor)570     public void setBackgroundTintColor(int backgroundColor) {
571         if (backgroundColor == mCurrentBackgroundColor) {
572             // Same color ignoring.
573            return;
574         }
575         mCurrentBackgroundColor = backgroundColor;
576 
577         final boolean dark = !NotificationColorUtil.isColorLight(backgroundColor);
578 
579         int textColor = NotificationColorUtil.ensureTextContrast(
580                 dark ? mDefaultTextColorDarkBg : mDefaultTextColor,
581                 backgroundColor | 0xff000000, dark);
582         int strokeColor = NotificationColorUtil.ensureContrast(
583                 mDefaultStrokeColor, backgroundColor | 0xff000000, dark, mMinStrokeContrast);
584         int rippleColor = dark ? mRippleColorDarkBg : mRippleColor;
585 
586         int childCount = getChildCount();
587         for (int i = 0; i < childCount; i++) {
588             final Button child = (Button) getChildAt(i);
589             setColors(child, backgroundColor, strokeColor, textColor, rippleColor);
590         }
591     }
592 
setColors(Button button, int backgroundColor, int strokeColor, int textColor, int rippleColor)593     private void setColors(Button button, int backgroundColor, int strokeColor, int textColor,
594             int rippleColor) {
595         Drawable drawable = button.getBackground();
596         if (drawable instanceof RippleDrawable) {
597             // Mutate in case other notifications are using this drawable.
598             drawable = drawable.mutate();
599             RippleDrawable ripple = (RippleDrawable) drawable;
600             ripple.setColor(ColorStateList.valueOf(rippleColor));
601             Drawable inset = ripple.getDrawable(0);
602             if (inset instanceof InsetDrawable) {
603                 Drawable background = ((InsetDrawable) inset).getDrawable();
604                 if (background instanceof GradientDrawable) {
605                     GradientDrawable gradientDrawable = (GradientDrawable) background;
606                     gradientDrawable.setColor(backgroundColor);
607                     gradientDrawable.setStroke(mStrokeWidth, strokeColor);
608                 }
609             }
610             button.setBackground(drawable);
611         }
612         button.setTextColor(textColor);
613     }
614 
615     @VisibleForTesting
616     static class LayoutParams extends ViewGroup.LayoutParams {
617 
618         /** Button is not squeezed. */
619         private static final int SQUEEZE_STATUS_NONE = 0;
620 
621         /**
622          * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
623          * turns out to have been unnecessary (because there's still not enough space to add another
624          * button).
625          */
626         private static final int SQUEEZE_STATUS_PENDING = 1;
627 
628         /** Button was successfully squeezed and it won't be un-squeezed. */
629         private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
630 
631         /**
632          * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
633          * text or it didn't reduce the button's width at all. The button will have to be
634          * re-measured to use only one line of text.
635          */
636         private static final int SQUEEZE_STATUS_FAILED = 3;
637 
638         private boolean show = false;
639         private int squeezeStatus = SQUEEZE_STATUS_NONE;
640 
LayoutParams(Context c, AttributeSet attrs)641         private LayoutParams(Context c, AttributeSet attrs) {
642             super(c, attrs);
643         }
644 
LayoutParams(int width, int height)645         private LayoutParams(int width, int height) {
646             super(width, height);
647         }
648 
649         @VisibleForTesting
isShown()650         boolean isShown() {
651             return show;
652         }
653     }
654 }
655