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