blob: 45d215ef309c72a1c42a691915c1fb109bd1e33c [file] [log] [blame]
Petr Cermaked7429c2017-12-18 19:38:04 +00001package com.android.systemui.statusbar.policy;
2
Kenny Guy14d035c2018-05-02 19:10:36 +01003import android.annotation.ColorInt;
Tony Mak29996702018-11-26 16:23:34 +00004import android.annotation.NonNull;
Gustav Senntoneab53682018-11-01 16:30:23 +00005import android.app.Notification;
Petr Cermaked7429c2017-12-18 19:38:04 +00006import android.app.PendingIntent;
7import android.app.RemoteInput;
8import android.content.Context;
9import android.content.Intent;
Kenny Guy14d035c2018-05-02 19:10:36 +010010import android.content.res.ColorStateList;
Petr Cermak102431d2018-01-29 10:36:07 +000011import android.content.res.TypedArray;
12import android.graphics.Canvas;
Kenny Guy14d035c2018-05-02 19:10:36 +010013import android.graphics.Color;
14import android.graphics.drawable.Drawable;
Petr Cermak102431d2018-01-29 10:36:07 +000015import android.graphics.drawable.GradientDrawable;
Kenny Guy14d035c2018-05-02 19:10:36 +010016import android.graphics.drawable.InsetDrawable;
Petr Cermak102431d2018-01-29 10:36:07 +000017import android.graphics.drawable.RippleDrawable;
Petr Cermaked7429c2017-12-18 19:38:04 +000018import android.os.Bundle;
Petr Cermak102431d2018-01-29 10:36:07 +000019import android.text.Layout;
20import android.text.TextPaint;
21import android.text.method.TransformationMethod;
Petr Cermaked7429c2017-12-18 19:38:04 +000022import android.util.AttributeSet;
23import android.util.Log;
24import android.view.LayoutInflater;
Petr Cermak102431d2018-01-29 10:36:07 +000025import android.view.View;
Petr Cermaked7429c2017-12-18 19:38:04 +000026import android.view.ViewGroup;
Milo Sredkov66da07b2018-04-17 14:04:54 +010027import android.view.accessibility.AccessibilityNodeInfo;
28import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Petr Cermaked7429c2017-12-18 19:38:04 +000029import android.widget.Button;
Petr Cermaked7429c2017-12-18 19:38:04 +000030
Petr Cermak102431d2018-01-29 10:36:07 +000031import com.android.internal.annotations.VisibleForTesting;
Lucas Dupina291d192018-06-07 13:59:42 -070032import com.android.internal.util.ContrastColorUtil;
Petr Cermak10011fa2018-02-05 19:00:54 +000033import com.android.systemui.Dependency;
Petr Cermaked7429c2017-12-18 19:38:04 +000034import com.android.systemui.R;
Gustav Senntoneab53682018-11-01 16:30:23 +000035import com.android.systemui.plugins.ActivityStarter;
Gus Prevasab336792018-11-14 13:52:20 -050036import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
Milo Sredkovb2af7f92018-12-04 15:22:18 +000037import com.android.systemui.statusbar.NotificationRemoteInputManager;
Kenny Guya0f6de82018-04-06 16:20:16 +010038import com.android.systemui.statusbar.SmartReplyController;
Milo Sredkove7cf4982018-04-09 15:08:26 +010039import com.android.systemui.statusbar.notification.NotificationUtils;
Ned Burnsf81c4c42019-01-07 14:10:43 -050040import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Milo Sredkov13d88112019-02-01 12:23:24 +000041import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
Gustav Sennton13edb492019-01-28 21:40:04 +000042import com.android.systemui.statusbar.notification.logging.NotificationLogger;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010043import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
Petr Cermaked7429c2017-12-18 19:38:04 +000044
Petr Cermak102431d2018-01-29 10:36:07 +000045import java.text.BreakIterator;
Gustav Senntonb149a1a2018-11-20 17:25:50 +000046import java.util.ArrayList;
Petr Cermak102431d2018-01-29 10:36:07 +000047import java.util.Comparator;
Gustav Senntoneab53682018-11-01 16:30:23 +000048import java.util.List;
Petr Cermak102431d2018-01-29 10:36:07 +000049import java.util.PriorityQueue;
50
Gustav Senntoneab53682018-11-01 16:30:23 +000051/** View which displays smart reply and smart actions buttons in notifications. */
Petr Cermak102431d2018-01-29 10:36:07 +000052public class SmartReplyView extends ViewGroup {
Petr Cermaked7429c2017-12-18 19:38:04 +000053
54 private static final String TAG = "SmartReplyView";
55
Gustav Senntoneab53682018-11-01 16:30:23 +000056 private static final int MEASURE_SPEC_ANY_LENGTH =
Petr Cermak102431d2018-01-29 10:36:07 +000057 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
58
59 private static final Comparator<View> DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR =
60 (v1, v2) -> ((v2.getMeasuredWidth() - v2.getPaddingLeft() - v2.getPaddingRight())
61 - (v1.getMeasuredWidth() - v1.getPaddingLeft() - v1.getPaddingRight()));
62
63 private static final int SQUEEZE_FAILED = -1;
64
Petr Cermak10011fa2018-02-05 19:00:54 +000065 private final SmartReplyConstants mConstants;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010066 private final KeyguardDismissUtil mKeyguardDismissUtil;
Milo Sredkovb2af7f92018-12-04 15:22:18 +000067 private final NotificationRemoteInputManager mRemoteInputManager;
Petr Cermak10011fa2018-02-05 19:00:54 +000068
Milo Sredkove7cf4982018-04-09 15:08:26 +010069 /**
70 * The upper bound for the height of this view in pixels. Notifications are automatically
71 * recreated on density or font size changes so caching this should be fine.
72 */
73 private final int mHeightUpperLimit;
74
Petr Cermak102431d2018-01-29 10:36:07 +000075 /** Spacing to be applied between views. */
76 private final int mSpacing;
77
78 /** Horizontal padding of smart reply buttons if all of them use only one line of text. */
79 private final int mSingleLineButtonPaddingHorizontal;
80
81 /** Horizontal padding of smart reply buttons if at least one of them uses two lines of text. */
82 private final int mDoubleLineButtonPaddingHorizontal;
83
84 /** Increase in width of a smart reply button as a result of using two lines instead of one. */
85 private final int mSingleToDoubleLineButtonWidthIncrease;
86
87 private final BreakIterator mBreakIterator;
88
89 private PriorityQueue<Button> mCandidateButtonQueueForSqueezing;
90
Kenny Guya0f6de82018-04-06 16:20:16 +010091 private View mSmartReplyContainer;
92
Gustav Senntona31f6ae2019-01-08 11:20:49 +000093 /**
94 * Whether the smart replies in this view were generated by the notification assistant. If not
95 * they're provided by the app.
96 */
97 private boolean mSmartRepliesGeneratedByAssistant = false;
98
Kenny Guy14d035c2018-05-02 19:10:36 +010099 @ColorInt
100 private int mCurrentBackgroundColor;
101 @ColorInt
102 private final int mDefaultBackgroundColor;
103 @ColorInt
104 private final int mDefaultStrokeColor;
105 @ColorInt
106 private final int mDefaultTextColor;
107 @ColorInt
108 private final int mDefaultTextColorDarkBg;
109 @ColorInt
110 private final int mRippleColorDarkBg;
111 @ColorInt
112 private final int mRippleColor;
113 private final int mStrokeWidth;
114 private final double mMinStrokeContrast;
115
Gustav Senntoneab53682018-11-01 16:30:23 +0000116 private ActivityStarter mActivityStarter;
117
Petr Cermaked7429c2017-12-18 19:38:04 +0000118 public SmartReplyView(Context context, AttributeSet attrs) {
119 super(context, attrs);
Petr Cermak10011fa2018-02-05 19:00:54 +0000120 mConstants = Dependency.get(SmartReplyConstants.class);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100121 mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000122 mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class);
Petr Cermak102431d2018-01-29 10:36:07 +0000123
Milo Sredkove7cf4982018-04-09 15:08:26 +0100124 mHeightUpperLimit = NotificationUtils.getFontScaledHeight(mContext,
125 R.dimen.smart_reply_button_max_height);
126
Kenny Guy14d035c2018-05-02 19:10:36 +0100127 mCurrentBackgroundColor = context.getColor(R.color.smart_reply_button_background);
128 mDefaultBackgroundColor = mCurrentBackgroundColor;
129 mDefaultTextColor = mContext.getColor(R.color.smart_reply_button_text);
130 mDefaultTextColorDarkBg = mContext.getColor(R.color.smart_reply_button_text_dark_bg);
131 mDefaultStrokeColor = mContext.getColor(R.color.smart_reply_button_stroke);
132 mRippleColor = mContext.getColor(R.color.notification_ripple_untinted_color);
133 mRippleColorDarkBg = Color.argb(Color.alpha(mRippleColor),
134 255 /* red */, 255 /* green */, 255 /* blue */);
Lucas Dupina291d192018-06-07 13:59:42 -0700135 mMinStrokeContrast = ContrastColorUtil.calculateContrast(mDefaultStrokeColor,
Kenny Guy14d035c2018-05-02 19:10:36 +0100136 mDefaultBackgroundColor);
137
Petr Cermak102431d2018-01-29 10:36:07 +0000138 int spacing = 0;
139 int singleLineButtonPaddingHorizontal = 0;
140 int doubleLineButtonPaddingHorizontal = 0;
Kenny Guy14d035c2018-05-02 19:10:36 +0100141 int strokeWidth = 0;
Petr Cermak102431d2018-01-29 10:36:07 +0000142
143 final TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.SmartReplyView,
144 0, 0);
145 final int length = arr.getIndexCount();
146 for (int i = 0; i < length; i++) {
147 int attr = arr.getIndex(i);
Jason Monk05dd5672018-08-09 09:38:21 -0400148 if (attr == R.styleable.SmartReplyView_spacing) {
149 spacing = arr.getDimensionPixelSize(i, 0);
150 } else if (attr == R.styleable.SmartReplyView_singleLineButtonPaddingHorizontal) {
151 singleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
152 } else if (attr == R.styleable.SmartReplyView_doubleLineButtonPaddingHorizontal) {
153 doubleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
154 } else if (attr == R.styleable.SmartReplyView_buttonStrokeWidth) {
155 strokeWidth = arr.getDimensionPixelSize(i, 0);
Petr Cermak102431d2018-01-29 10:36:07 +0000156 }
157 }
158 arr.recycle();
159
Kenny Guy14d035c2018-05-02 19:10:36 +0100160 mStrokeWidth = strokeWidth;
Petr Cermak102431d2018-01-29 10:36:07 +0000161 mSpacing = spacing;
162 mSingleLineButtonPaddingHorizontal = singleLineButtonPaddingHorizontal;
163 mDoubleLineButtonPaddingHorizontal = doubleLineButtonPaddingHorizontal;
164 mSingleToDoubleLineButtonWidthIncrease =
165 2 * (doubleLineButtonPaddingHorizontal - singleLineButtonPaddingHorizontal);
166
Milo Sredkove7cf4982018-04-09 15:08:26 +0100167
Petr Cermak102431d2018-01-29 10:36:07 +0000168 mBreakIterator = BreakIterator.getLineInstance();
169 reallocateCandidateButtonQueueForSqueezing();
170 }
171
Milo Sredkove7cf4982018-04-09 15:08:26 +0100172 /**
173 * Returns an upper bound for the height of this view in pixels. This method is intended to be
174 * invoked before onMeasure, so it doesn't do any analysis on the contents of the buttons.
175 */
176 public int getHeightUpperLimit() {
177 return mHeightUpperLimit;
178 }
179
Petr Cermak102431d2018-01-29 10:36:07 +0000180 private void reallocateCandidateButtonQueueForSqueezing() {
181 // Instead of clearing the priority queue, we re-allocate so that it would fit all buttons
182 // exactly. This avoids (1) wasting memory because PriorityQueue never shrinks and
183 // (2) growing in onMeasure.
184 // The constructor throws an IllegalArgument exception if initial capacity is less than 1.
185 mCandidateButtonQueueForSqueezing = new PriorityQueue<>(
186 Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
Petr Cermaked7429c2017-12-18 19:38:04 +0000187 }
188
Gustav Senntoneab53682018-11-01 16:30:23 +0000189 /**
190 * Reset the smart suggestions view to allow adding new replies and actions.
191 */
192 public void resetSmartSuggestions(View newSmartReplyContainer) {
193 mSmartReplyContainer = newSmartReplyContainer;
Petr Cermaked7429c2017-12-18 19:38:04 +0000194 removeAllViews();
Kenny Guy14d035c2018-05-02 19:10:36 +0100195 mCurrentBackgroundColor = mDefaultBackgroundColor;
Gustav Senntoneab53682018-11-01 16:30:23 +0000196 }
197
198 /**
199 * Add smart replies to this view, using the provided {@link RemoteInput} and
200 * {@link PendingIntent} to respond when the user taps a smart reply. Only the replies that fit
201 * into the notification are shown.
202 */
203 public void addRepliesFromRemoteInput(
Tony Mak29996702018-11-26 16:23:34 +0000204 SmartReplies smartReplies,
Ned Burnsf81c4c42019-01-07 14:10:43 -0500205 SmartReplyController smartReplyController, NotificationEntry entry) {
Tony Mak29996702018-11-26 16:23:34 +0000206 if (smartReplies.remoteInput != null && smartReplies.pendingIntent != null) {
207 if (smartReplies.choices != null) {
208 for (int i = 0; i < smartReplies.choices.length; ++i) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000209 Button replyButton = inflateReplyButton(
Tony Mak29996702018-11-26 16:23:34 +0000210 getContext(), this, i, smartReplies, smartReplyController, entry);
Petr Cermaked7429c2017-12-18 19:38:04 +0000211 addView(replyButton);
212 }
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000213 this.mSmartRepliesGeneratedByAssistant = smartReplies.fromAssistant;
Petr Cermaked7429c2017-12-18 19:38:04 +0000214 }
215 }
Petr Cermak102431d2018-01-29 10:36:07 +0000216 reallocateCandidateButtonQueueForSqueezing();
Petr Cermaked7429c2017-12-18 19:38:04 +0000217 }
218
Gustav Senntoneab53682018-11-01 16:30:23 +0000219 /**
220 * Add smart actions to be shown next to smart replies. Only the actions that fit into the
221 * notification are shown.
222 */
Tony Mak7d4b3a52018-11-27 17:29:36 +0000223 public void addSmartActions(SmartActions smartActions,
Ned Burnsf81c4c42019-01-07 14:10:43 -0500224 SmartReplyController smartReplyController, NotificationEntry entry,
Gustav Senntond0e84532018-12-03 16:48:36 +0000225 HeadsUpManager headsUpManager) {
Tony Mak29996702018-11-26 16:23:34 +0000226 int numSmartActions = smartActions.actions.size();
Gustav Senntoneab53682018-11-01 16:30:23 +0000227 for (int n = 0; n < numSmartActions; n++) {
Tony Mak29996702018-11-26 16:23:34 +0000228 Notification.Action action = smartActions.actions.get(n);
Gustav Senntoneab53682018-11-01 16:30:23 +0000229 if (action.actionIntent != null) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000230 Button actionButton = inflateActionButton(
Gustav Senntond0e84532018-12-03 16:48:36 +0000231 getContext(), this, n, smartActions, smartReplyController, entry,
232 headsUpManager);
Gustav Senntoneab53682018-11-01 16:30:23 +0000233 addView(actionButton);
234 }
235 }
236 reallocateCandidateButtonQueueForSqueezing();
237 }
238
Petr Cermaked7429c2017-12-18 19:38:04 +0000239 public static SmartReplyView inflate(Context context, ViewGroup root) {
240 return (SmartReplyView)
241 LayoutInflater.from(context).inflate(R.layout.smart_reply_view, root, false);
242 }
243
Petr Cermak102431d2018-01-29 10:36:07 +0000244 @VisibleForTesting
Kenny Guy23991102018-04-05 21:18:38 +0100245 Button inflateReplyButton(Context context, ViewGroup root, int replyIndex,
Tony Mak29996702018-11-26 16:23:34 +0000246 SmartReplies smartReplies, SmartReplyController smartReplyController,
Ned Burnsf81c4c42019-01-07 14:10:43 -0500247 NotificationEntry entry) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000248 Button b = (Button) LayoutInflater.from(context).inflate(
249 R.layout.smart_reply_button, root, false);
Tony Mak29996702018-11-26 16:23:34 +0000250 CharSequence choice = smartReplies.choices[replyIndex];
Petr Cermaked7429c2017-12-18 19:38:04 +0000251 b.setText(choice);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100252
253 OnDismissAction action = () -> {
Milo Sredkov41dc4ba2018-12-27 12:03:45 +0000254 if (mConstants.getEffectiveEditChoicesBeforeSending(
255 smartReplies.remoteInput.getEditChoicesBeforeSending())) {
Milo Sredkov13d88112019-02-01 12:23:24 +0000256 EditedSuggestionInfo editedSuggestionInfo =
257 new EditedSuggestionInfo(choice, replyIndex);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000258 mRemoteInputManager.activateRemoteInput(b,
259 new RemoteInput[] { smartReplies.remoteInput }, smartReplies.remoteInput,
Milo Sredkov13d88112019-02-01 12:23:24 +0000260 smartReplies.pendingIntent, editedSuggestionInfo);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000261 return false;
262 }
263
Gustav Sennton13edb492019-01-28 21:40:04 +0000264 smartReplyController.smartReplySent(entry, replyIndex, b.getText(),
Milo Sredkov13d88112019-02-01 12:23:24 +0000265 NotificationLogger.getNotificationLocation(entry).toMetricsEventEnum(),
266 false /* modifiedBeforeSending */);
Petr Cermaked7429c2017-12-18 19:38:04 +0000267 Bundle results = new Bundle();
Tony Mak29996702018-11-26 16:23:34 +0000268 results.putString(smartReplies.remoteInput.getResultKey(), choice.toString());
Petr Cermaked7429c2017-12-18 19:38:04 +0000269 Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000270 RemoteInput.addResultsToIntent(new RemoteInput[] { smartReplies.remoteInput }, intent,
Tony Mak29996702018-11-26 16:23:34 +0000271 results);
Petr Cermak9a3380c2018-01-19 15:00:24 +0000272 RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
Selim Cinekb0dc61b2018-05-22 18:49:36 -0700273 entry.setHasSentReply();
Petr Cermaked7429c2017-12-18 19:38:04 +0000274 try {
Tony Mak29996702018-11-26 16:23:34 +0000275 smartReplies.pendingIntent.send(context, 0, intent);
Petr Cermaked7429c2017-12-18 19:38:04 +0000276 } catch (PendingIntent.CanceledException e) {
277 Log.w(TAG, "Unable to send smart reply", e);
278 }
Kenny Guya0f6de82018-04-06 16:20:16 +0100279 mSmartReplyContainer.setVisibility(View.GONE);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100280 return false; // do not defer
281 };
282
283 b.setOnClickListener(view -> {
Milo Sredkove433e9b2018-05-01 22:45:38 +0100284 mKeyguardDismissUtil.executeWhenUnlocked(action);
Petr Cermaked7429c2017-12-18 19:38:04 +0000285 });
Milo Sredkov66da07b2018-04-17 14:04:54 +0100286
287 b.setAccessibilityDelegate(new AccessibilityDelegate() {
288 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
289 super.onInitializeAccessibilityNodeInfo(host, info);
290 String label = getResources().getString(R.string.accessibility_send_smart_reply);
291 info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
292 }
293 });
294
Kenny Guy14d035c2018-05-02 19:10:36 +0100295 setColors(b, mCurrentBackgroundColor, mDefaultStrokeColor, mDefaultTextColor, mRippleColor);
Petr Cermaked7429c2017-12-18 19:38:04 +0000296 return b;
297 }
Petr Cermak102431d2018-01-29 10:36:07 +0000298
Gustav Senntoneab53682018-11-01 16:30:23 +0000299 @VisibleForTesting
Tony Mak7d4b3a52018-11-27 17:29:36 +0000300 Button inflateActionButton(Context context, ViewGroup root, int actionIndex,
301 SmartActions smartActions, SmartReplyController smartReplyController,
Ned Burnsf81c4c42019-01-07 14:10:43 -0500302 NotificationEntry entry, HeadsUpManager headsUpManager) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000303 Notification.Action action = smartActions.actions.get(actionIndex);
Gustav Senntoneab53682018-11-01 16:30:23 +0000304 Button button = (Button) LayoutInflater.from(context).inflate(
305 R.layout.smart_action_button, root, false);
306 button.setText(action.title);
307
308 Drawable iconDrawable = action.getIcon().loadDrawable(context);
309 // Add the action icon to the Smart Action button.
Gustav Senntonc726f472018-12-04 11:57:06 +0000310 int newIconSize = context.getResources().getDimensionPixelSize(
311 R.dimen.smart_action_button_icon_size);
312 iconDrawable.setBounds(0, 0, newIconSize, newIconSize);
Gustav Senntoneab53682018-11-01 16:30:23 +0000313 button.setCompoundDrawables(iconDrawable, null, null, null);
314
315 button.setOnClickListener(view ->
Tony Mak7d4b3a52018-11-27 17:29:36 +0000316 getActivityStarter().startPendingIntentDismissingKeyguard(
317 action.actionIntent,
Gustav Senntond0e84532018-12-03 16:48:36 +0000318 () -> {
319 smartReplyController.smartActionClicked(
320 entry, actionIndex, action, smartActions.fromAssistant);
Gustav Sennton32137e42018-12-17 12:26:46 +0000321 headsUpManager.removeNotification(entry.key, true);
Gustav Senntond0e84532018-12-03 16:48:36 +0000322 }));
Gustav Senntoneab53682018-11-01 16:30:23 +0000323
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000324 // Mark this as an Action button
325 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
326 lp.buttonType = SmartButtonType.ACTION;
Gustav Senntoneab53682018-11-01 16:30:23 +0000327 return button;
328 }
329
Petr Cermak102431d2018-01-29 10:36:07 +0000330 @Override
331 public LayoutParams generateLayoutParams(AttributeSet attrs) {
332 return new LayoutParams(mContext, attrs);
333 }
334
335 @Override
336 protected LayoutParams generateDefaultLayoutParams() {
337 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
338 }
339
340 @Override
341 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
342 return new LayoutParams(params.width, params.height);
343 }
344
345 @Override
346 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
347 final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
348 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
349
350 // Mark all buttons as hidden and un-squeezed.
351 resetButtonsLayoutParams();
352
353 if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
354 Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
355 mCandidateButtonQueueForSqueezing.clear();
356 }
357
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000358 SmartSuggestionMeasures accumulatedMeasures = new SmartSuggestionMeasures(
359 mPaddingLeft + mPaddingRight,
360 0 /* maxChildHeight */,
361 mSingleLineButtonPaddingHorizontal);
Petr Cermak102431d2018-01-29 10:36:07 +0000362 int displayedChildCount = 0;
Petr Cermak102431d2018-01-29 10:36:07 +0000363
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000364 // Set up a list of suggestions where actions come before replies. Note that the Buttons
365 // themselves have already been added to the view hierarchy in an order such that Smart
366 // Replies are shown before Smart Actions. The order of the list below determines which
367 // suggestions will be shown at all - only the first X elements are shown (where X depends
368 // on how much space each suggestion button needs).
369 List<View> smartActions = filterActionsOrReplies(SmartButtonType.ACTION);
370 List<View> smartReplies = filterActionsOrReplies(SmartButtonType.REPLY);
371 List<View> smartSuggestions = new ArrayList<>(smartActions);
372 smartSuggestions.addAll(smartReplies);
373 List<View> coveredSuggestions = new ArrayList<>();
374
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000375 // SmartSuggestionMeasures for all action buttons, this will be filled in when the first
376 // reply button is added.
377 SmartSuggestionMeasures actionsMeasures = null;
378
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000379 final int maxNumActions = mConstants.getMaxNumActions();
380 int numShownActions = 0;
381
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000382 for (View child : smartSuggestions) {
Petr Cermak102431d2018-01-29 10:36:07 +0000383 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000384 if (maxNumActions != -1 // -1 means 'no limit'
385 && lp.buttonType == SmartButtonType.ACTION
386 && numShownActions >= maxNumActions) {
387 // We've reached the maximum number of actions, don't add another one!
388 continue;
389 }
Petr Cermak102431d2018-01-29 10:36:07 +0000390
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000391 child.setPadding(accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingTop(),
392 accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingBottom());
Gustav Senntoneab53682018-11-01 16:30:23 +0000393 child.measure(MEASURE_SPEC_ANY_LENGTH, heightMeasureSpec);
Petr Cermak102431d2018-01-29 10:36:07 +0000394
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000395 coveredSuggestions.add(child);
396
Petr Cermak102431d2018-01-29 10:36:07 +0000397 final int lineCount = ((Button) child).getLineCount();
398 if (lineCount < 1 || lineCount > 2) {
399 // If smart reply has no text, or more than two lines, then don't show it.
400 continue;
401 }
402
403 if (lineCount == 1) {
404 mCandidateButtonQueueForSqueezing.add((Button) child);
405 }
406
407 // Remember the current measurements in case the current button doesn't fit in.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000408 SmartSuggestionMeasures originalMeasures = accumulatedMeasures.clone();
409 if (actionsMeasures == null && lp.buttonType == SmartButtonType.REPLY) {
410 // We've added all actions (we go through actions first), now add their
411 // measurements.
412 actionsMeasures = accumulatedMeasures.clone();
413 }
Petr Cermak102431d2018-01-29 10:36:07 +0000414
415 final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
416 final int childWidth = child.getMeasuredWidth();
417 final int childHeight = child.getMeasuredHeight();
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000418 accumulatedMeasures.mMeasuredWidth += spacing + childWidth;
419 accumulatedMeasures.mMaxChildHeight =
420 Math.max(accumulatedMeasures.mMaxChildHeight, childHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000421
422 // Do we need to increase the number of lines in smart reply buttons to two?
423 final boolean increaseToTwoLines =
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000424 (accumulatedMeasures.mButtonPaddingHorizontal
425 == mSingleLineButtonPaddingHorizontal)
426 && (lineCount == 2 || accumulatedMeasures.mMeasuredWidth > targetWidth);
Petr Cermak102431d2018-01-29 10:36:07 +0000427 if (increaseToTwoLines) {
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000428 accumulatedMeasures.mMeasuredWidth +=
429 (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease;
430 accumulatedMeasures.mButtonPaddingHorizontal =
431 mDoubleLineButtonPaddingHorizontal;
Petr Cermak102431d2018-01-29 10:36:07 +0000432 }
433
434 // If the last button doesn't fit into the remaining width, try squeezing preceding
435 // smart reply buttons.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000436 if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
Petr Cermak102431d2018-01-29 10:36:07 +0000437 // Keep squeezing preceding and current smart reply buttons until they all fit.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000438 while (accumulatedMeasures.mMeasuredWidth > targetWidth
Petr Cermak102431d2018-01-29 10:36:07 +0000439 && !mCandidateButtonQueueForSqueezing.isEmpty()) {
440 final Button candidate = mCandidateButtonQueueForSqueezing.poll();
441 final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
442 if (squeezeReduction != SQUEEZE_FAILED) {
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000443 accumulatedMeasures.mMaxChildHeight =
444 Math.max(accumulatedMeasures.mMaxChildHeight,
445 candidate.getMeasuredHeight());
446 accumulatedMeasures.mMeasuredWidth -= squeezeReduction;
Petr Cermak102431d2018-01-29 10:36:07 +0000447 }
448 }
449
450 // If the current button still doesn't fit after squeezing all buttons, undo the
451 // last squeezing round.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000452 if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
453 accumulatedMeasures = originalMeasures;
Petr Cermak102431d2018-01-29 10:36:07 +0000454
455 // Mark all buttons from the last squeezing round as "failed to squeeze", so
456 // that they're re-measured without squeezing later.
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000457 markButtonsWithPendingSqueezeStatusAs(
458 LayoutParams.SQUEEZE_STATUS_FAILED, coveredSuggestions);
Petr Cermak102431d2018-01-29 10:36:07 +0000459
Gustav Sennton35156752018-12-20 10:35:03 +0000460 // The current button doesn't fit, keep on adding lower-priority buttons in case
461 // any of those fit.
462 continue;
Petr Cermak102431d2018-01-29 10:36:07 +0000463 }
464
465 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
466 // to prevent them from being un-squeezed in a subsequent squeezing round.
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000467 markButtonsWithPendingSqueezeStatusAs(
468 LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, coveredSuggestions);
Petr Cermak102431d2018-01-29 10:36:07 +0000469 }
470
471 lp.show = true;
472 displayedChildCount++;
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000473 if (lp.buttonType == SmartButtonType.ACTION) {
474 numShownActions++;
475 }
Petr Cermak102431d2018-01-29 10:36:07 +0000476 }
477
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000478 if (mSmartRepliesGeneratedByAssistant) {
479 if (!gotEnoughSmartReplies(smartReplies)) {
480 // We don't have enough smart replies - hide all of them.
481 for (View smartReplyButton : smartReplies) {
482 final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
483 lp.show = false;
484 }
485 // Reset our measures back to when we had only added actions (before adding
486 // replies).
487 accumulatedMeasures = actionsMeasures;
488 }
489 }
490
Petr Cermak102431d2018-01-29 10:36:07 +0000491 // We're done squeezing buttons, so we can clear the priority queue.
492 mCandidateButtonQueueForSqueezing.clear();
493
Milo Sredkova5bacea2018-04-12 12:52:43 +0100494 // Finally, we need to re-measure some buttons.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000495 remeasureButtonsIfNecessary(accumulatedMeasures.mButtonPaddingHorizontal,
496 accumulatedMeasures.mMaxChildHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000497
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000498 int buttonHeight = Math.max(getSuggestedMinimumHeight(), mPaddingTop
499 + accumulatedMeasures.mMaxChildHeight + mPaddingBottom);
500
501 // Set the corner radius to half the button height to make the side of the buttons look like
502 // a semicircle.
503 for (View smartSuggestionButton : smartSuggestions) {
504 setCornerRadius((Button) smartSuggestionButton, ((float) buttonHeight) / 2);
505 }
506
Petr Cermak102431d2018-01-29 10:36:07 +0000507 setMeasuredDimension(
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000508 resolveSize(Math.max(getSuggestedMinimumWidth(),
509 accumulatedMeasures.mMeasuredWidth),
510 widthMeasureSpec),
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000511 resolveSize(buttonHeight, heightMeasureSpec));
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000512 }
513
514 /**
515 * Fields we keep track of inside onMeasure() to correctly measure the SmartReplyView depending
516 * on which suggestions are added.
517 */
518 private static class SmartSuggestionMeasures {
519 int mMeasuredWidth = -1;
520 int mMaxChildHeight = -1;
521 int mButtonPaddingHorizontal = -1;
522
523 SmartSuggestionMeasures(int measuredWidth, int maxChildHeight,
524 int buttonPaddingHorizontal) {
525 this.mMeasuredWidth = measuredWidth;
526 this.mMaxChildHeight = maxChildHeight;
527 this.mButtonPaddingHorizontal = buttonPaddingHorizontal;
528 }
529
530 public SmartSuggestionMeasures clone() {
531 return new SmartSuggestionMeasures(
532 mMeasuredWidth, mMaxChildHeight, mButtonPaddingHorizontal);
533 }
534 }
535
536 /**
537 * Returns whether our notification contains at least N smart replies (or 0) where N is
538 * determined by {@link SmartReplyConstants}.
539 */
540 private boolean gotEnoughSmartReplies(List<View> smartReplies) {
541 int numShownReplies = 0;
542 for (View smartReplyButton : smartReplies) {
543 final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
544 if (lp.show) {
545 numShownReplies++;
546 }
547 }
548 if (numShownReplies == 0
549 || numShownReplies >= mConstants.getMinNumSystemGeneratedReplies()) {
550 // We have enough replies, yay!
551 return true;
552 }
553 return false;
Petr Cermak102431d2018-01-29 10:36:07 +0000554 }
555
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000556 private List<View> filterActionsOrReplies(SmartButtonType buttonType) {
557 List<View> actions = new ArrayList<>();
558 final int childCount = getChildCount();
559 for (int i = 0; i < childCount; i++) {
560 final View child = getChildAt(i);
561 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
562 if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
563 continue;
564 }
565 if (lp.buttonType == buttonType) {
566 actions.add(child);
567 }
568 }
569 return actions;
570 }
571
Petr Cermak102431d2018-01-29 10:36:07 +0000572 private void resetButtonsLayoutParams() {
573 final int childCount = getChildCount();
574 for (int i = 0; i < childCount; i++) {
575 final View child = getChildAt(i);
576 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
577 lp.show = false;
578 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
579 }
580 }
581
582 private int squeezeButton(Button button, int heightMeasureSpec) {
583 final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
584 if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
585 return SQUEEZE_FAILED;
586 }
587 return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
588 }
589
590 private int estimateOptimalSqueezedButtonTextWidth(Button button) {
591 // Find a line-break point in the middle of the smart reply button text.
592 final String rawText = button.getText().toString();
593
594 // The button sometimes has a transformation affecting text layout (e.g. all caps).
595 final TransformationMethod transformation = button.getTransformationMethod();
596 final String text = transformation == null ?
597 rawText : transformation.getTransformation(rawText, button).toString();
598 final int length = text.length();
599 mBreakIterator.setText(text);
600
601 if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
602 if (mBreakIterator.next() == BreakIterator.DONE) {
603 // Can't find a single possible line break in either direction.
604 return SQUEEZE_FAILED;
605 }
606 }
607
608 final TextPaint paint = button.getPaint();
609 final int initialPosition = mBreakIterator.current();
610 final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
611 final float initialRightTextWidth =
612 Layout.getDesiredWidth(text, initialPosition, length, paint);
613 float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
614
615 if (initialLeftTextWidth != initialRightTextWidth) {
616 // See if there's a better line-break point (leading to a more narrow button) in
617 // either left or right direction.
618 final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
619 final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
620 for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
621 final int newPosition =
622 moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
623 if (newPosition == BreakIterator.DONE) {
624 break;
625 }
626
627 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
628 final float newRightTextWidth =
629 Layout.getDesiredWidth(text, newPosition, length, paint);
630 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
631 if (newOptimalTextWidth < optimalTextWidth) {
632 optimalTextWidth = newOptimalTextWidth;
633 } else {
634 break;
635 }
636
637 boolean tooFar = moveLeft
638 ? newLeftTextWidth <= newRightTextWidth
639 : newLeftTextWidth >= newRightTextWidth;
640 if (tooFar) {
641 break;
642 }
643 }
644 }
645
646 return (int) Math.ceil(optimalTextWidth);
647 }
648
Gustav Senntoneab53682018-11-01 16:30:23 +0000649 /**
650 * Returns the combined width of the left drawable (the action icon) and the padding between the
651 * drawable and the button text.
652 */
653 private int getLeftCompoundDrawableWidthWithPadding(Button button) {
654 Drawable[] drawables = button.getCompoundDrawables();
655 Drawable leftDrawable = drawables[0];
656 if (leftDrawable == null) return 0;
657
658 return leftDrawable.getBounds().width() + button.getCompoundDrawablePadding();
659 }
660
Petr Cermak102431d2018-01-29 10:36:07 +0000661 private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
662 int oldWidth = button.getMeasuredWidth();
663 if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
664 // Correct for the fact that the button was laid out with single-line horizontal
665 // padding.
666 oldWidth += mSingleToDoubleLineButtonWidthIncrease;
667 }
668
669 // Re-measure the squeezed smart reply button.
670 button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
671 mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
672 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
Gustav Senntoneab53682018-11-01 16:30:23 +0000673 2 * mDoubleLineButtonPaddingHorizontal + textWidth
674 + getLeftCompoundDrawableWidthWithPadding(button), MeasureSpec.AT_MOST);
Petr Cermak102431d2018-01-29 10:36:07 +0000675 button.measure(widthMeasureSpec, heightMeasureSpec);
676
677 final int newWidth = button.getMeasuredWidth();
678
679 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
680 if (button.getLineCount() > 2 || newWidth >= oldWidth) {
681 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
682 return SQUEEZE_FAILED;
683 } else {
684 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
685 return oldWidth - newWidth;
686 }
687 }
688
Milo Sredkova5bacea2018-04-12 12:52:43 +0100689 private void remeasureButtonsIfNecessary(
Petr Cermak102431d2018-01-29 10:36:07 +0000690 int buttonPaddingHorizontal, int maxChildHeight) {
Petr Cermak102431d2018-01-29 10:36:07 +0000691 final int maxChildHeightMeasure =
692 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
693
694 final int childCount = getChildCount();
695 for (int i = 0; i < childCount; i++) {
696 final View child = getChildAt(i);
697 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
698 if (!lp.show) {
699 continue;
700 }
701
Petr Cermak102431d2018-01-29 10:36:07 +0000702 boolean requiresNewMeasure = false;
703 int newWidth = child.getMeasuredWidth();
704
705 // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
706 // in more than two lines or because it was unnecessary).
707 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
708 requiresNewMeasure = true;
709 newWidth = Integer.MAX_VALUE;
710 }
711
712 // Re-measure reason 2: The button's horizontal padding is incorrect (because it was
713 // measured with the wrong number of lines).
714 if (child.getPaddingLeft() != buttonPaddingHorizontal) {
715 requiresNewMeasure = true;
Kenny Guy48ee6d62018-05-09 16:51:26 +0100716 if (newWidth != Integer.MAX_VALUE) {
717 if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) {
718 // Change padding (2->1 line).
719 newWidth -= mSingleToDoubleLineButtonWidthIncrease;
720 } else {
721 // Change padding (1->2 lines).
722 newWidth += mSingleToDoubleLineButtonWidthIncrease;
723 }
Petr Cermak102431d2018-01-29 10:36:07 +0000724 }
725 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
726 buttonPaddingHorizontal, child.getPaddingBottom());
727 }
728
729 // Re-measure reason 3: The button's height is less than the max height of all buttons
730 // (all should have the same height).
731 if (child.getMeasuredHeight() != maxChildHeight) {
732 requiresNewMeasure = true;
733 }
734
735 if (requiresNewMeasure) {
736 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
737 maxChildHeightMeasure);
738 }
739 }
740 }
741
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000742 private void markButtonsWithPendingSqueezeStatusAs(
743 int squeezeStatus, List<View> coveredChildren) {
744 for (View child : coveredChildren) {
Petr Cermak102431d2018-01-29 10:36:07 +0000745 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
746 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
747 lp.squeezeStatus = squeezeStatus;
748 }
749 }
750 }
751
752 @Override
753 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
754 final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
755
756 final int width = right - left;
757 int position = isRtl ? width - mPaddingRight : mPaddingLeft;
758
759 final int childCount = getChildCount();
760 for (int i = 0; i < childCount; i++) {
761 final View child = getChildAt(i);
762 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
763 if (!lp.show) {
764 continue;
765 }
766
767 final int childWidth = child.getMeasuredWidth();
768 final int childHeight = child.getMeasuredHeight();
769 final int childLeft = isRtl ? position - childWidth : position;
770 child.layout(childLeft, 0, childLeft + childWidth, childHeight);
771
772 final int childWidthWithSpacing = childWidth + mSpacing;
773 if (isRtl) {
774 position -= childWidthWithSpacing;
775 } else {
776 position += childWidthWithSpacing;
777 }
778 }
779 }
780
781 @Override
782 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
783 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
784 return lp.show && super.drawChild(canvas, child, drawingTime);
785 }
786
Kenny Guy14d035c2018-05-02 19:10:36 +0100787 public void setBackgroundTintColor(int backgroundColor) {
788 if (backgroundColor == mCurrentBackgroundColor) {
789 // Same color ignoring.
790 return;
791 }
792 mCurrentBackgroundColor = backgroundColor;
793
Lucas Dupina291d192018-06-07 13:59:42 -0700794 final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100795
Lucas Dupina291d192018-06-07 13:59:42 -0700796 int textColor = ContrastColorUtil.ensureTextContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100797 dark ? mDefaultTextColorDarkBg : mDefaultTextColor,
798 backgroundColor | 0xff000000, dark);
Lucas Dupina291d192018-06-07 13:59:42 -0700799 int strokeColor = ContrastColorUtil.ensureContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100800 mDefaultStrokeColor, backgroundColor | 0xff000000, dark, mMinStrokeContrast);
801 int rippleColor = dark ? mRippleColorDarkBg : mRippleColor;
802
803 int childCount = getChildCount();
804 for (int i = 0; i < childCount; i++) {
805 final Button child = (Button) getChildAt(i);
806 setColors(child, backgroundColor, strokeColor, textColor, rippleColor);
807 }
808 }
809
810 private void setColors(Button button, int backgroundColor, int strokeColor, int textColor,
811 int rippleColor) {
812 Drawable drawable = button.getBackground();
813 if (drawable instanceof RippleDrawable) {
814 // Mutate in case other notifications are using this drawable.
815 drawable = drawable.mutate();
816 RippleDrawable ripple = (RippleDrawable) drawable;
817 ripple.setColor(ColorStateList.valueOf(rippleColor));
818 Drawable inset = ripple.getDrawable(0);
819 if (inset instanceof InsetDrawable) {
820 Drawable background = ((InsetDrawable) inset).getDrawable();
821 if (background instanceof GradientDrawable) {
822 GradientDrawable gradientDrawable = (GradientDrawable) background;
823 gradientDrawable.setColor(backgroundColor);
824 gradientDrawable.setStroke(mStrokeWidth, strokeColor);
825 }
826 }
827 button.setBackground(drawable);
828 }
829 button.setTextColor(textColor);
830 }
831
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000832 private void setCornerRadius(Button button, float radius) {
833 Drawable drawable = button.getBackground();
834 if (drawable instanceof RippleDrawable) {
835 // Mutate in case other notifications are using this drawable.
836 drawable = drawable.mutate();
837 RippleDrawable ripple = (RippleDrawable) drawable;
838 Drawable inset = ripple.getDrawable(0);
839 if (inset instanceof InsetDrawable) {
840 Drawable background = ((InsetDrawable) inset).getDrawable();
841 if (background instanceof GradientDrawable) {
842 GradientDrawable gradientDrawable = (GradientDrawable) background;
843 gradientDrawable.setCornerRadius(radius);
844 }
845 }
846 }
847 }
848
Gustav Senntoneab53682018-11-01 16:30:23 +0000849 private ActivityStarter getActivityStarter() {
850 if (mActivityStarter == null) {
851 mActivityStarter = Dependency.get(ActivityStarter.class);
852 }
853 return mActivityStarter;
854 }
855
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000856 private enum SmartButtonType {
857 REPLY,
858 ACTION
859 }
860
Petr Cermak102431d2018-01-29 10:36:07 +0000861 @VisibleForTesting
862 static class LayoutParams extends ViewGroup.LayoutParams {
863
864 /** Button is not squeezed. */
865 private static final int SQUEEZE_STATUS_NONE = 0;
866
867 /**
868 * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
869 * turns out to have been unnecessary (because there's still not enough space to add another
870 * button).
871 */
872 private static final int SQUEEZE_STATUS_PENDING = 1;
873
874 /** Button was successfully squeezed and it won't be un-squeezed. */
875 private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
876
877 /**
878 * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
879 * text or it didn't reduce the button's width at all. The button will have to be
880 * re-measured to use only one line of text.
881 */
882 private static final int SQUEEZE_STATUS_FAILED = 3;
883
884 private boolean show = false;
885 private int squeezeStatus = SQUEEZE_STATUS_NONE;
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000886 private SmartButtonType buttonType = SmartButtonType.REPLY;
Petr Cermak102431d2018-01-29 10:36:07 +0000887
888 private LayoutParams(Context c, AttributeSet attrs) {
889 super(c, attrs);
890 }
891
892 private LayoutParams(int width, int height) {
893 super(width, height);
894 }
895
896 @VisibleForTesting
897 boolean isShown() {
898 return show;
899 }
900 }
Tony Mak29996702018-11-26 16:23:34 +0000901
902 /**
903 * Data class for smart replies.
904 */
905 public static class SmartReplies {
906 @NonNull
907 public final RemoteInput remoteInput;
908 @NonNull
909 public final PendingIntent pendingIntent;
910 @NonNull
911 public final CharSequence[] choices;
912 public final boolean fromAssistant;
913
914 public SmartReplies(CharSequence[] choices, RemoteInput remoteInput,
915 PendingIntent pendingIntent, boolean fromAssistant) {
916 this.choices = choices;
917 this.remoteInput = remoteInput;
918 this.pendingIntent = pendingIntent;
919 this.fromAssistant = fromAssistant;
920 }
921 }
922
923
924 /**
925 * Data class for smart actions.
926 */
927 public static class SmartActions {
928 @NonNull
929 public final List<Notification.Action> actions;
930 public final boolean fromAssistant;
931
932 public SmartActions(List<Notification.Action> actions, boolean fromAssistant) {
933 this.actions = actions;
934 this.fromAssistant = fromAssistant;
935 }
936 }
Petr Cermaked7429c2017-12-18 19:38:04 +0000937}