blob: ed5487f743561b9785ccb3423bd3a47cee0c45d8 [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 /**
Gustav Sennton5759f872019-02-13 17:25:26 +0000199 * Add buttons to the {@link SmartReplyView} - these buttons must have been preinflated using
200 * one of the methods in this class.
201 */
202 public void addPreInflatedButtons(List<Button> smartSuggestionButtons) {
203 for (Button button : smartSuggestionButtons) {
204 addView(button);
205 }
206 reallocateCandidateButtonQueueForSqueezing();
207 }
208
209 /**
Gustav Senntoneab53682018-11-01 16:30:23 +0000210 * Add smart replies to this view, using the provided {@link RemoteInput} and
211 * {@link PendingIntent} to respond when the user taps a smart reply. Only the replies that fit
212 * into the notification are shown.
213 */
Gustav Sennton5759f872019-02-13 17:25:26 +0000214 public List<Button> inflateRepliesFromRemoteInput(
215 @NonNull SmartReplies smartReplies,
Ned Burnsf81c4c42019-01-07 14:10:43 -0500216 SmartReplyController smartReplyController, NotificationEntry entry) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000217 List<Button> buttons = new ArrayList<>();
218
Tony Mak29996702018-11-26 16:23:34 +0000219 if (smartReplies.remoteInput != null && smartReplies.pendingIntent != null) {
220 if (smartReplies.choices != null) {
221 for (int i = 0; i < smartReplies.choices.length; ++i) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000222 buttons.add(inflateReplyButton(
223 this, getContext(), i, smartReplies, smartReplyController, entry));
Petr Cermaked7429c2017-12-18 19:38:04 +0000224 }
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000225 this.mSmartRepliesGeneratedByAssistant = smartReplies.fromAssistant;
Petr Cermaked7429c2017-12-18 19:38:04 +0000226 }
227 }
Gustav Sennton5759f872019-02-13 17:25:26 +0000228 return buttons;
Petr Cermaked7429c2017-12-18 19:38:04 +0000229 }
230
Gustav Senntoneab53682018-11-01 16:30:23 +0000231 /**
232 * Add smart actions to be shown next to smart replies. Only the actions that fit into the
233 * notification are shown.
234 */
Gustav Sennton5759f872019-02-13 17:25:26 +0000235 public List<Button> inflateSmartActions(@NonNull SmartActions smartActions,
Ned Burnsf81c4c42019-01-07 14:10:43 -0500236 SmartReplyController smartReplyController, NotificationEntry entry,
Gustav Senntond0e84532018-12-03 16:48:36 +0000237 HeadsUpManager headsUpManager) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000238 List<Button> buttons = new ArrayList<>();
Tony Mak29996702018-11-26 16:23:34 +0000239 int numSmartActions = smartActions.actions.size();
Gustav Senntoneab53682018-11-01 16:30:23 +0000240 for (int n = 0; n < numSmartActions; n++) {
Tony Mak29996702018-11-26 16:23:34 +0000241 Notification.Action action = smartActions.actions.get(n);
Gustav Senntoneab53682018-11-01 16:30:23 +0000242 if (action.actionIntent != null) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000243 buttons.add(inflateActionButton(
244 this, getContext(), n, smartActions, smartReplyController, entry,
245 headsUpManager));
Gustav Senntoneab53682018-11-01 16:30:23 +0000246 }
247 }
Gustav Sennton5759f872019-02-13 17:25:26 +0000248 return buttons;
Gustav Senntoneab53682018-11-01 16:30:23 +0000249 }
250
Gustav Sennton5759f872019-02-13 17:25:26 +0000251 /**
252 * Inflate an instance of this class.
253 */
254 public static SmartReplyView inflate(Context context) {
255 return (SmartReplyView) LayoutInflater.from(context).inflate(
256 R.layout.smart_reply_view, null /* root */);
Petr Cermaked7429c2017-12-18 19:38:04 +0000257 }
258
Petr Cermak102431d2018-01-29 10:36:07 +0000259 @VisibleForTesting
Gustav Sennton5759f872019-02-13 17:25:26 +0000260 static Button inflateReplyButton(SmartReplyView smartReplyView, Context context,
261 int replyIndex, SmartReplies smartReplies, SmartReplyController smartReplyController,
Ned Burnsf81c4c42019-01-07 14:10:43 -0500262 NotificationEntry entry) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000263 Button b = (Button) LayoutInflater.from(context).inflate(
Gustav Sennton5759f872019-02-13 17:25:26 +0000264 R.layout.smart_reply_button, smartReplyView, false);
Tony Mak29996702018-11-26 16:23:34 +0000265 CharSequence choice = smartReplies.choices[replyIndex];
Petr Cermaked7429c2017-12-18 19:38:04 +0000266 b.setText(choice);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100267
268 OnDismissAction action = () -> {
Gustav Sennton5759f872019-02-13 17:25:26 +0000269 if (smartReplyView.mConstants.getEffectiveEditChoicesBeforeSending(
Milo Sredkov41dc4ba2018-12-27 12:03:45 +0000270 smartReplies.remoteInput.getEditChoicesBeforeSending())) {
Milo Sredkov13d88112019-02-01 12:23:24 +0000271 EditedSuggestionInfo editedSuggestionInfo =
272 new EditedSuggestionInfo(choice, replyIndex);
Gustav Sennton5759f872019-02-13 17:25:26 +0000273 smartReplyView.mRemoteInputManager.activateRemoteInput(b,
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000274 new RemoteInput[] { smartReplies.remoteInput }, smartReplies.remoteInput,
Milo Sredkov13d88112019-02-01 12:23:24 +0000275 smartReplies.pendingIntent, editedSuggestionInfo);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000276 return false;
277 }
278
Gustav Sennton13edb492019-01-28 21:40:04 +0000279 smartReplyController.smartReplySent(entry, replyIndex, b.getText(),
Milo Sredkov13d88112019-02-01 12:23:24 +0000280 NotificationLogger.getNotificationLocation(entry).toMetricsEventEnum(),
281 false /* modifiedBeforeSending */);
Petr Cermaked7429c2017-12-18 19:38:04 +0000282 Bundle results = new Bundle();
Tony Mak29996702018-11-26 16:23:34 +0000283 results.putString(smartReplies.remoteInput.getResultKey(), choice.toString());
Petr Cermaked7429c2017-12-18 19:38:04 +0000284 Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000285 RemoteInput.addResultsToIntent(new RemoteInput[] { smartReplies.remoteInput }, intent,
Tony Mak29996702018-11-26 16:23:34 +0000286 results);
Petr Cermak9a3380c2018-01-19 15:00:24 +0000287 RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
Selim Cinekb0dc61b2018-05-22 18:49:36 -0700288 entry.setHasSentReply();
Petr Cermaked7429c2017-12-18 19:38:04 +0000289 try {
Tony Mak29996702018-11-26 16:23:34 +0000290 smartReplies.pendingIntent.send(context, 0, intent);
Petr Cermaked7429c2017-12-18 19:38:04 +0000291 } catch (PendingIntent.CanceledException e) {
292 Log.w(TAG, "Unable to send smart reply", e);
293 }
Gustav Sennton5759f872019-02-13 17:25:26 +0000294 // Note that as inflateReplyButton is called mSmartReplyContainer is null, but when the
295 // reply Button is added to the SmartReplyView mSmartReplyContainer will be set. So, it
296 // will not be possible for a user to trigger this on-click-listener without
297 // mSmartReplyContainer being set.
298 smartReplyView.mSmartReplyContainer.setVisibility(View.GONE);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100299 return false; // do not defer
300 };
301
302 b.setOnClickListener(view -> {
Gustav Sennton5759f872019-02-13 17:25:26 +0000303 smartReplyView.mKeyguardDismissUtil.executeWhenUnlocked(action);
Petr Cermaked7429c2017-12-18 19:38:04 +0000304 });
Milo Sredkov66da07b2018-04-17 14:04:54 +0100305
306 b.setAccessibilityDelegate(new AccessibilityDelegate() {
307 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
308 super.onInitializeAccessibilityNodeInfo(host, info);
Gustav Sennton5759f872019-02-13 17:25:26 +0000309 String label = smartReplyView.getResources().getString(
310 R.string.accessibility_send_smart_reply);
Milo Sredkov66da07b2018-04-17 14:04:54 +0100311 info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
312 }
313 });
314
Gustav Sennton5759f872019-02-13 17:25:26 +0000315 SmartReplyView.setButtonColors(b, smartReplyView.mCurrentBackgroundColor,
316 smartReplyView.mDefaultStrokeColor, smartReplyView.mDefaultTextColor,
317 smartReplyView.mRippleColor, smartReplyView.mStrokeWidth);
Petr Cermaked7429c2017-12-18 19:38:04 +0000318 return b;
319 }
Petr Cermak102431d2018-01-29 10:36:07 +0000320
Gustav Senntoneab53682018-11-01 16:30:23 +0000321 @VisibleForTesting
Gustav Sennton5759f872019-02-13 17:25:26 +0000322 static Button inflateActionButton(SmartReplyView smartReplyView, Context context,
323 int actionIndex, SmartActions smartActions,
324 SmartReplyController smartReplyController, NotificationEntry entry,
325 HeadsUpManager headsUpManager) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000326 Notification.Action action = smartActions.actions.get(actionIndex);
Gustav Senntoneab53682018-11-01 16:30:23 +0000327 Button button = (Button) LayoutInflater.from(context).inflate(
Gustav Sennton5759f872019-02-13 17:25:26 +0000328 R.layout.smart_action_button, smartReplyView, false);
Gustav Senntoneab53682018-11-01 16:30:23 +0000329 button.setText(action.title);
330
331 Drawable iconDrawable = action.getIcon().loadDrawable(context);
332 // Add the action icon to the Smart Action button.
Gustav Senntonc726f472018-12-04 11:57:06 +0000333 int newIconSize = context.getResources().getDimensionPixelSize(
334 R.dimen.smart_action_button_icon_size);
335 iconDrawable.setBounds(0, 0, newIconSize, newIconSize);
Gustav Senntoneab53682018-11-01 16:30:23 +0000336 button.setCompoundDrawables(iconDrawable, null, null, null);
337
338 button.setOnClickListener(view ->
Gustav Sennton5759f872019-02-13 17:25:26 +0000339 smartReplyView.getActivityStarter().startPendingIntentDismissingKeyguard(
Tony Mak7d4b3a52018-11-27 17:29:36 +0000340 action.actionIntent,
Gustav Senntond0e84532018-12-03 16:48:36 +0000341 () -> {
342 smartReplyController.smartActionClicked(
343 entry, actionIndex, action, smartActions.fromAssistant);
Gustav Sennton32137e42018-12-17 12:26:46 +0000344 headsUpManager.removeNotification(entry.key, true);
Gustav Senntond0e84532018-12-03 16:48:36 +0000345 }));
Gustav Senntoneab53682018-11-01 16:30:23 +0000346
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000347 // Mark this as an Action button
348 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
349 lp.buttonType = SmartButtonType.ACTION;
Gustav Senntoneab53682018-11-01 16:30:23 +0000350 return button;
351 }
352
Petr Cermak102431d2018-01-29 10:36:07 +0000353 @Override
354 public LayoutParams generateLayoutParams(AttributeSet attrs) {
355 return new LayoutParams(mContext, attrs);
356 }
357
358 @Override
359 protected LayoutParams generateDefaultLayoutParams() {
360 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
361 }
362
363 @Override
364 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
365 return new LayoutParams(params.width, params.height);
366 }
367
368 @Override
369 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
370 final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
371 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
372
373 // Mark all buttons as hidden and un-squeezed.
374 resetButtonsLayoutParams();
375
376 if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
377 Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
378 mCandidateButtonQueueForSqueezing.clear();
379 }
380
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000381 SmartSuggestionMeasures accumulatedMeasures = new SmartSuggestionMeasures(
382 mPaddingLeft + mPaddingRight,
383 0 /* maxChildHeight */,
384 mSingleLineButtonPaddingHorizontal);
Petr Cermak102431d2018-01-29 10:36:07 +0000385 int displayedChildCount = 0;
Petr Cermak102431d2018-01-29 10:36:07 +0000386
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000387 // Set up a list of suggestions where actions come before replies. Note that the Buttons
388 // themselves have already been added to the view hierarchy in an order such that Smart
389 // Replies are shown before Smart Actions. The order of the list below determines which
390 // suggestions will be shown at all - only the first X elements are shown (where X depends
391 // on how much space each suggestion button needs).
392 List<View> smartActions = filterActionsOrReplies(SmartButtonType.ACTION);
393 List<View> smartReplies = filterActionsOrReplies(SmartButtonType.REPLY);
394 List<View> smartSuggestions = new ArrayList<>(smartActions);
395 smartSuggestions.addAll(smartReplies);
396 List<View> coveredSuggestions = new ArrayList<>();
397
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000398 // SmartSuggestionMeasures for all action buttons, this will be filled in when the first
399 // reply button is added.
400 SmartSuggestionMeasures actionsMeasures = null;
401
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000402 final int maxNumActions = mConstants.getMaxNumActions();
403 int numShownActions = 0;
404
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000405 for (View child : smartSuggestions) {
Petr Cermak102431d2018-01-29 10:36:07 +0000406 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000407 if (maxNumActions != -1 // -1 means 'no limit'
408 && lp.buttonType == SmartButtonType.ACTION
409 && numShownActions >= maxNumActions) {
410 // We've reached the maximum number of actions, don't add another one!
411 continue;
412 }
Petr Cermak102431d2018-01-29 10:36:07 +0000413
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000414 child.setPadding(accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingTop(),
415 accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingBottom());
Gustav Senntoneab53682018-11-01 16:30:23 +0000416 child.measure(MEASURE_SPEC_ANY_LENGTH, heightMeasureSpec);
Petr Cermak102431d2018-01-29 10:36:07 +0000417
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000418 coveredSuggestions.add(child);
419
Petr Cermak102431d2018-01-29 10:36:07 +0000420 final int lineCount = ((Button) child).getLineCount();
421 if (lineCount < 1 || lineCount > 2) {
422 // If smart reply has no text, or more than two lines, then don't show it.
423 continue;
424 }
425
426 if (lineCount == 1) {
427 mCandidateButtonQueueForSqueezing.add((Button) child);
428 }
429
430 // Remember the current measurements in case the current button doesn't fit in.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000431 SmartSuggestionMeasures originalMeasures = accumulatedMeasures.clone();
432 if (actionsMeasures == null && lp.buttonType == SmartButtonType.REPLY) {
433 // We've added all actions (we go through actions first), now add their
434 // measurements.
435 actionsMeasures = accumulatedMeasures.clone();
436 }
Petr Cermak102431d2018-01-29 10:36:07 +0000437
438 final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
439 final int childWidth = child.getMeasuredWidth();
440 final int childHeight = child.getMeasuredHeight();
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000441 accumulatedMeasures.mMeasuredWidth += spacing + childWidth;
442 accumulatedMeasures.mMaxChildHeight =
443 Math.max(accumulatedMeasures.mMaxChildHeight, childHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000444
445 // Do we need to increase the number of lines in smart reply buttons to two?
446 final boolean increaseToTwoLines =
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000447 (accumulatedMeasures.mButtonPaddingHorizontal
448 == mSingleLineButtonPaddingHorizontal)
449 && (lineCount == 2 || accumulatedMeasures.mMeasuredWidth > targetWidth);
Petr Cermak102431d2018-01-29 10:36:07 +0000450 if (increaseToTwoLines) {
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000451 accumulatedMeasures.mMeasuredWidth +=
452 (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease;
453 accumulatedMeasures.mButtonPaddingHorizontal =
454 mDoubleLineButtonPaddingHorizontal;
Petr Cermak102431d2018-01-29 10:36:07 +0000455 }
456
457 // If the last button doesn't fit into the remaining width, try squeezing preceding
458 // smart reply buttons.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000459 if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
Petr Cermak102431d2018-01-29 10:36:07 +0000460 // Keep squeezing preceding and current smart reply buttons until they all fit.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000461 while (accumulatedMeasures.mMeasuredWidth > targetWidth
Petr Cermak102431d2018-01-29 10:36:07 +0000462 && !mCandidateButtonQueueForSqueezing.isEmpty()) {
463 final Button candidate = mCandidateButtonQueueForSqueezing.poll();
464 final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
465 if (squeezeReduction != SQUEEZE_FAILED) {
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000466 accumulatedMeasures.mMaxChildHeight =
467 Math.max(accumulatedMeasures.mMaxChildHeight,
468 candidate.getMeasuredHeight());
469 accumulatedMeasures.mMeasuredWidth -= squeezeReduction;
Petr Cermak102431d2018-01-29 10:36:07 +0000470 }
471 }
472
473 // If the current button still doesn't fit after squeezing all buttons, undo the
474 // last squeezing round.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000475 if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
476 accumulatedMeasures = originalMeasures;
Petr Cermak102431d2018-01-29 10:36:07 +0000477
478 // Mark all buttons from the last squeezing round as "failed to squeeze", so
479 // that they're re-measured without squeezing later.
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000480 markButtonsWithPendingSqueezeStatusAs(
481 LayoutParams.SQUEEZE_STATUS_FAILED, coveredSuggestions);
Petr Cermak102431d2018-01-29 10:36:07 +0000482
Gustav Sennton35156752018-12-20 10:35:03 +0000483 // The current button doesn't fit, keep on adding lower-priority buttons in case
484 // any of those fit.
485 continue;
Petr Cermak102431d2018-01-29 10:36:07 +0000486 }
487
488 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
489 // to prevent them from being un-squeezed in a subsequent squeezing round.
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000490 markButtonsWithPendingSqueezeStatusAs(
491 LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, coveredSuggestions);
Petr Cermak102431d2018-01-29 10:36:07 +0000492 }
493
494 lp.show = true;
495 displayedChildCount++;
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000496 if (lp.buttonType == SmartButtonType.ACTION) {
497 numShownActions++;
498 }
Petr Cermak102431d2018-01-29 10:36:07 +0000499 }
500
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000501 if (mSmartRepliesGeneratedByAssistant) {
502 if (!gotEnoughSmartReplies(smartReplies)) {
503 // We don't have enough smart replies - hide all of them.
504 for (View smartReplyButton : smartReplies) {
505 final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
506 lp.show = false;
507 }
508 // Reset our measures back to when we had only added actions (before adding
509 // replies).
510 accumulatedMeasures = actionsMeasures;
511 }
512 }
513
Petr Cermak102431d2018-01-29 10:36:07 +0000514 // We're done squeezing buttons, so we can clear the priority queue.
515 mCandidateButtonQueueForSqueezing.clear();
516
Milo Sredkova5bacea2018-04-12 12:52:43 +0100517 // Finally, we need to re-measure some buttons.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000518 remeasureButtonsIfNecessary(accumulatedMeasures.mButtonPaddingHorizontal,
519 accumulatedMeasures.mMaxChildHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000520
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000521 int buttonHeight = Math.max(getSuggestedMinimumHeight(), mPaddingTop
522 + accumulatedMeasures.mMaxChildHeight + mPaddingBottom);
523
524 // Set the corner radius to half the button height to make the side of the buttons look like
525 // a semicircle.
526 for (View smartSuggestionButton : smartSuggestions) {
527 setCornerRadius((Button) smartSuggestionButton, ((float) buttonHeight) / 2);
528 }
529
Petr Cermak102431d2018-01-29 10:36:07 +0000530 setMeasuredDimension(
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000531 resolveSize(Math.max(getSuggestedMinimumWidth(),
532 accumulatedMeasures.mMeasuredWidth),
533 widthMeasureSpec),
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000534 resolveSize(buttonHeight, heightMeasureSpec));
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000535 }
536
537 /**
538 * Fields we keep track of inside onMeasure() to correctly measure the SmartReplyView depending
539 * on which suggestions are added.
540 */
541 private static class SmartSuggestionMeasures {
542 int mMeasuredWidth = -1;
543 int mMaxChildHeight = -1;
544 int mButtonPaddingHorizontal = -1;
545
546 SmartSuggestionMeasures(int measuredWidth, int maxChildHeight,
547 int buttonPaddingHorizontal) {
548 this.mMeasuredWidth = measuredWidth;
549 this.mMaxChildHeight = maxChildHeight;
550 this.mButtonPaddingHorizontal = buttonPaddingHorizontal;
551 }
552
553 public SmartSuggestionMeasures clone() {
554 return new SmartSuggestionMeasures(
555 mMeasuredWidth, mMaxChildHeight, mButtonPaddingHorizontal);
556 }
557 }
558
559 /**
560 * Returns whether our notification contains at least N smart replies (or 0) where N is
561 * determined by {@link SmartReplyConstants}.
562 */
563 private boolean gotEnoughSmartReplies(List<View> smartReplies) {
564 int numShownReplies = 0;
565 for (View smartReplyButton : smartReplies) {
566 final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
567 if (lp.show) {
568 numShownReplies++;
569 }
570 }
571 if (numShownReplies == 0
572 || numShownReplies >= mConstants.getMinNumSystemGeneratedReplies()) {
573 // We have enough replies, yay!
574 return true;
575 }
576 return false;
Petr Cermak102431d2018-01-29 10:36:07 +0000577 }
578
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000579 private List<View> filterActionsOrReplies(SmartButtonType buttonType) {
580 List<View> actions = new ArrayList<>();
581 final int childCount = getChildCount();
582 for (int i = 0; i < childCount; i++) {
583 final View child = getChildAt(i);
584 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
585 if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
586 continue;
587 }
588 if (lp.buttonType == buttonType) {
589 actions.add(child);
590 }
591 }
592 return actions;
593 }
594
Petr Cermak102431d2018-01-29 10:36:07 +0000595 private void resetButtonsLayoutParams() {
596 final int childCount = getChildCount();
597 for (int i = 0; i < childCount; i++) {
598 final View child = getChildAt(i);
599 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
600 lp.show = false;
601 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
602 }
603 }
604
605 private int squeezeButton(Button button, int heightMeasureSpec) {
606 final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
607 if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
608 return SQUEEZE_FAILED;
609 }
610 return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
611 }
612
613 private int estimateOptimalSqueezedButtonTextWidth(Button button) {
614 // Find a line-break point in the middle of the smart reply button text.
615 final String rawText = button.getText().toString();
616
617 // The button sometimes has a transformation affecting text layout (e.g. all caps).
618 final TransformationMethod transformation = button.getTransformationMethod();
619 final String text = transformation == null ?
620 rawText : transformation.getTransformation(rawText, button).toString();
621 final int length = text.length();
622 mBreakIterator.setText(text);
623
624 if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
625 if (mBreakIterator.next() == BreakIterator.DONE) {
626 // Can't find a single possible line break in either direction.
627 return SQUEEZE_FAILED;
628 }
629 }
630
631 final TextPaint paint = button.getPaint();
632 final int initialPosition = mBreakIterator.current();
633 final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
634 final float initialRightTextWidth =
635 Layout.getDesiredWidth(text, initialPosition, length, paint);
636 float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
637
638 if (initialLeftTextWidth != initialRightTextWidth) {
639 // See if there's a better line-break point (leading to a more narrow button) in
640 // either left or right direction.
641 final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
642 final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
643 for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
644 final int newPosition =
645 moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
646 if (newPosition == BreakIterator.DONE) {
647 break;
648 }
649
650 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
651 final float newRightTextWidth =
652 Layout.getDesiredWidth(text, newPosition, length, paint);
653 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
654 if (newOptimalTextWidth < optimalTextWidth) {
655 optimalTextWidth = newOptimalTextWidth;
656 } else {
657 break;
658 }
659
660 boolean tooFar = moveLeft
661 ? newLeftTextWidth <= newRightTextWidth
662 : newLeftTextWidth >= newRightTextWidth;
663 if (tooFar) {
664 break;
665 }
666 }
667 }
668
669 return (int) Math.ceil(optimalTextWidth);
670 }
671
Gustav Senntoneab53682018-11-01 16:30:23 +0000672 /**
673 * Returns the combined width of the left drawable (the action icon) and the padding between the
674 * drawable and the button text.
675 */
676 private int getLeftCompoundDrawableWidthWithPadding(Button button) {
677 Drawable[] drawables = button.getCompoundDrawables();
678 Drawable leftDrawable = drawables[0];
679 if (leftDrawable == null) return 0;
680
681 return leftDrawable.getBounds().width() + button.getCompoundDrawablePadding();
682 }
683
Petr Cermak102431d2018-01-29 10:36:07 +0000684 private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
685 int oldWidth = button.getMeasuredWidth();
686 if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
687 // Correct for the fact that the button was laid out with single-line horizontal
688 // padding.
689 oldWidth += mSingleToDoubleLineButtonWidthIncrease;
690 }
691
692 // Re-measure the squeezed smart reply button.
693 button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
694 mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
695 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
Gustav Senntoneab53682018-11-01 16:30:23 +0000696 2 * mDoubleLineButtonPaddingHorizontal + textWidth
697 + getLeftCompoundDrawableWidthWithPadding(button), MeasureSpec.AT_MOST);
Petr Cermak102431d2018-01-29 10:36:07 +0000698 button.measure(widthMeasureSpec, heightMeasureSpec);
699
700 final int newWidth = button.getMeasuredWidth();
701
702 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
703 if (button.getLineCount() > 2 || newWidth >= oldWidth) {
704 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
705 return SQUEEZE_FAILED;
706 } else {
707 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
708 return oldWidth - newWidth;
709 }
710 }
711
Milo Sredkova5bacea2018-04-12 12:52:43 +0100712 private void remeasureButtonsIfNecessary(
Petr Cermak102431d2018-01-29 10:36:07 +0000713 int buttonPaddingHorizontal, int maxChildHeight) {
Petr Cermak102431d2018-01-29 10:36:07 +0000714 final int maxChildHeightMeasure =
715 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
716
717 final int childCount = getChildCount();
718 for (int i = 0; i < childCount; i++) {
719 final View child = getChildAt(i);
720 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
721 if (!lp.show) {
722 continue;
723 }
724
Petr Cermak102431d2018-01-29 10:36:07 +0000725 boolean requiresNewMeasure = false;
726 int newWidth = child.getMeasuredWidth();
727
728 // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
729 // in more than two lines or because it was unnecessary).
730 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
731 requiresNewMeasure = true;
732 newWidth = Integer.MAX_VALUE;
733 }
734
735 // Re-measure reason 2: The button's horizontal padding is incorrect (because it was
736 // measured with the wrong number of lines).
737 if (child.getPaddingLeft() != buttonPaddingHorizontal) {
738 requiresNewMeasure = true;
Kenny Guy48ee6d62018-05-09 16:51:26 +0100739 if (newWidth != Integer.MAX_VALUE) {
740 if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) {
741 // Change padding (2->1 line).
742 newWidth -= mSingleToDoubleLineButtonWidthIncrease;
743 } else {
744 // Change padding (1->2 lines).
745 newWidth += mSingleToDoubleLineButtonWidthIncrease;
746 }
Petr Cermak102431d2018-01-29 10:36:07 +0000747 }
748 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
749 buttonPaddingHorizontal, child.getPaddingBottom());
750 }
751
752 // Re-measure reason 3: The button's height is less than the max height of all buttons
753 // (all should have the same height).
754 if (child.getMeasuredHeight() != maxChildHeight) {
755 requiresNewMeasure = true;
756 }
757
758 if (requiresNewMeasure) {
759 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
760 maxChildHeightMeasure);
761 }
762 }
763 }
764
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000765 private void markButtonsWithPendingSqueezeStatusAs(
766 int squeezeStatus, List<View> coveredChildren) {
767 for (View child : coveredChildren) {
Petr Cermak102431d2018-01-29 10:36:07 +0000768 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
769 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
770 lp.squeezeStatus = squeezeStatus;
771 }
772 }
773 }
774
775 @Override
776 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
777 final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
778
779 final int width = right - left;
780 int position = isRtl ? width - mPaddingRight : mPaddingLeft;
781
782 final int childCount = getChildCount();
783 for (int i = 0; i < childCount; i++) {
784 final View child = getChildAt(i);
785 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
786 if (!lp.show) {
787 continue;
788 }
789
790 final int childWidth = child.getMeasuredWidth();
791 final int childHeight = child.getMeasuredHeight();
792 final int childLeft = isRtl ? position - childWidth : position;
793 child.layout(childLeft, 0, childLeft + childWidth, childHeight);
794
795 final int childWidthWithSpacing = childWidth + mSpacing;
796 if (isRtl) {
797 position -= childWidthWithSpacing;
798 } else {
799 position += childWidthWithSpacing;
800 }
801 }
802 }
803
804 @Override
805 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
806 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
807 return lp.show && super.drawChild(canvas, child, drawingTime);
808 }
809
Kenny Guy14d035c2018-05-02 19:10:36 +0100810 public void setBackgroundTintColor(int backgroundColor) {
811 if (backgroundColor == mCurrentBackgroundColor) {
812 // Same color ignoring.
813 return;
814 }
815 mCurrentBackgroundColor = backgroundColor;
816
Lucas Dupina291d192018-06-07 13:59:42 -0700817 final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100818
Lucas Dupina291d192018-06-07 13:59:42 -0700819 int textColor = ContrastColorUtil.ensureTextContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100820 dark ? mDefaultTextColorDarkBg : mDefaultTextColor,
821 backgroundColor | 0xff000000, dark);
Lucas Dupina291d192018-06-07 13:59:42 -0700822 int strokeColor = ContrastColorUtil.ensureContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100823 mDefaultStrokeColor, backgroundColor | 0xff000000, dark, mMinStrokeContrast);
824 int rippleColor = dark ? mRippleColorDarkBg : mRippleColor;
825
826 int childCount = getChildCount();
827 for (int i = 0; i < childCount; i++) {
828 final Button child = (Button) getChildAt(i);
Gustav Sennton5759f872019-02-13 17:25:26 +0000829 setButtonColors(child, backgroundColor, strokeColor, textColor, rippleColor,
830 mStrokeWidth);
Kenny Guy14d035c2018-05-02 19:10:36 +0100831 }
832 }
833
Gustav Sennton5759f872019-02-13 17:25:26 +0000834 private static void setButtonColors(Button button, int backgroundColor, int strokeColor,
835 int textColor, int rippleColor, int strokeWidth) {
Kenny Guy14d035c2018-05-02 19:10:36 +0100836 Drawable drawable = button.getBackground();
837 if (drawable instanceof RippleDrawable) {
838 // Mutate in case other notifications are using this drawable.
839 drawable = drawable.mutate();
840 RippleDrawable ripple = (RippleDrawable) drawable;
841 ripple.setColor(ColorStateList.valueOf(rippleColor));
842 Drawable inset = ripple.getDrawable(0);
843 if (inset instanceof InsetDrawable) {
844 Drawable background = ((InsetDrawable) inset).getDrawable();
845 if (background instanceof GradientDrawable) {
846 GradientDrawable gradientDrawable = (GradientDrawable) background;
847 gradientDrawable.setColor(backgroundColor);
Gustav Sennton5759f872019-02-13 17:25:26 +0000848 gradientDrawable.setStroke(strokeWidth, strokeColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100849 }
850 }
851 button.setBackground(drawable);
852 }
853 button.setTextColor(textColor);
854 }
855
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000856 private void setCornerRadius(Button button, float radius) {
857 Drawable drawable = button.getBackground();
858 if (drawable instanceof RippleDrawable) {
859 // Mutate in case other notifications are using this drawable.
860 drawable = drawable.mutate();
861 RippleDrawable ripple = (RippleDrawable) drawable;
862 Drawable inset = ripple.getDrawable(0);
863 if (inset instanceof InsetDrawable) {
864 Drawable background = ((InsetDrawable) inset).getDrawable();
865 if (background instanceof GradientDrawable) {
866 GradientDrawable gradientDrawable = (GradientDrawable) background;
867 gradientDrawable.setCornerRadius(radius);
868 }
869 }
870 }
871 }
872
Gustav Senntoneab53682018-11-01 16:30:23 +0000873 private ActivityStarter getActivityStarter() {
874 if (mActivityStarter == null) {
875 mActivityStarter = Dependency.get(ActivityStarter.class);
876 }
877 return mActivityStarter;
878 }
879
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000880 private enum SmartButtonType {
881 REPLY,
882 ACTION
883 }
884
Petr Cermak102431d2018-01-29 10:36:07 +0000885 @VisibleForTesting
886 static class LayoutParams extends ViewGroup.LayoutParams {
887
888 /** Button is not squeezed. */
889 private static final int SQUEEZE_STATUS_NONE = 0;
890
891 /**
892 * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
893 * turns out to have been unnecessary (because there's still not enough space to add another
894 * button).
895 */
896 private static final int SQUEEZE_STATUS_PENDING = 1;
897
898 /** Button was successfully squeezed and it won't be un-squeezed. */
899 private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
900
901 /**
902 * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
903 * text or it didn't reduce the button's width at all. The button will have to be
904 * re-measured to use only one line of text.
905 */
906 private static final int SQUEEZE_STATUS_FAILED = 3;
907
908 private boolean show = false;
909 private int squeezeStatus = SQUEEZE_STATUS_NONE;
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000910 private SmartButtonType buttonType = SmartButtonType.REPLY;
Petr Cermak102431d2018-01-29 10:36:07 +0000911
912 private LayoutParams(Context c, AttributeSet attrs) {
913 super(c, attrs);
914 }
915
916 private LayoutParams(int width, int height) {
917 super(width, height);
918 }
919
920 @VisibleForTesting
921 boolean isShown() {
922 return show;
923 }
924 }
Tony Mak29996702018-11-26 16:23:34 +0000925
926 /**
927 * Data class for smart replies.
928 */
929 public static class SmartReplies {
930 @NonNull
931 public final RemoteInput remoteInput;
932 @NonNull
933 public final PendingIntent pendingIntent;
934 @NonNull
935 public final CharSequence[] choices;
936 public final boolean fromAssistant;
937
938 public SmartReplies(CharSequence[] choices, RemoteInput remoteInput,
939 PendingIntent pendingIntent, boolean fromAssistant) {
940 this.choices = choices;
941 this.remoteInput = remoteInput;
942 this.pendingIntent = pendingIntent;
943 this.fromAssistant = fromAssistant;
944 }
945 }
946
947
948 /**
949 * Data class for smart actions.
950 */
951 public static class SmartActions {
952 @NonNull
953 public final List<Notification.Action> actions;
954 public final boolean fromAssistant;
955
956 public SmartActions(List<Notification.Action> actions, boolean fromAssistant) {
957 this.actions = actions;
958 this.fromAssistant = fromAssistant;
959 }
960 }
Petr Cermaked7429c2017-12-18 19:38:04 +0000961}