blob: 282d28c3b20c32df1aa31cc0e18eb61db4d1fd43 [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;
Gustav Sennton8a52dc32019-04-15 12:48:23 +010019import android.os.SystemClock;
Petr Cermak102431d2018-01-29 10:36:07 +000020import android.text.Layout;
21import android.text.TextPaint;
22import android.text.method.TransformationMethod;
Petr Cermaked7429c2017-12-18 19:38:04 +000023import android.util.AttributeSet;
24import android.util.Log;
25import android.view.LayoutInflater;
Petr Cermak102431d2018-01-29 10:36:07 +000026import android.view.View;
Petr Cermaked7429c2017-12-18 19:38:04 +000027import android.view.ViewGroup;
Milo Sredkov66da07b2018-04-17 14:04:54 +010028import android.view.accessibility.AccessibilityNodeInfo;
29import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
Petr Cermaked7429c2017-12-18 19:38:04 +000030import android.widget.Button;
Petr Cermaked7429c2017-12-18 19:38:04 +000031
Petr Cermak102431d2018-01-29 10:36:07 +000032import com.android.internal.annotations.VisibleForTesting;
Lucas Dupina291d192018-06-07 13:59:42 -070033import com.android.internal.util.ContrastColorUtil;
Petr Cermak10011fa2018-02-05 19:00:54 +000034import com.android.systemui.Dependency;
Petr Cermaked7429c2017-12-18 19:38:04 +000035import com.android.systemui.R;
Gustav Senntoneab53682018-11-01 16:30:23 +000036import com.android.systemui.plugins.ActivityStarter;
Gus Prevasab336792018-11-14 13:52:20 -050037import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
Milo Sredkovb2af7f92018-12-04 15:22:18 +000038import com.android.systemui.statusbar.NotificationRemoteInputManager;
Kenny Guya0f6de82018-04-06 16:20:16 +010039import com.android.systemui.statusbar.SmartReplyController;
Milo Sredkove7cf4982018-04-09 15:08:26 +010040import com.android.systemui.statusbar.notification.NotificationUtils;
Ned Burnsf81c4c42019-01-07 14:10:43 -050041import com.android.systemui.statusbar.notification.collection.NotificationEntry;
Milo Sredkov13d88112019-02-01 12:23:24 +000042import com.android.systemui.statusbar.notification.collection.NotificationEntry.EditedSuggestionInfo;
Gustav Sennton13edb492019-01-28 21:40:04 +000043import com.android.systemui.statusbar.notification.logging.NotificationLogger;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010044import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
Petr Cermaked7429c2017-12-18 19:38:04 +000045
Petr Cermak102431d2018-01-29 10:36:07 +000046import java.text.BreakIterator;
Gustav Senntonb149a1a2018-11-20 17:25:50 +000047import java.util.ArrayList;
Petr Cermak102431d2018-01-29 10:36:07 +000048import java.util.Comparator;
Gustav Senntoneab53682018-11-01 16:30:23 +000049import java.util.List;
Petr Cermak102431d2018-01-29 10:36:07 +000050import java.util.PriorityQueue;
51
Gustav Senntoneab53682018-11-01 16:30:23 +000052/** View which displays smart reply and smart actions buttons in notifications. */
Petr Cermak102431d2018-01-29 10:36:07 +000053public class SmartReplyView extends ViewGroup {
Petr Cermaked7429c2017-12-18 19:38:04 +000054
55 private static final String TAG = "SmartReplyView";
56
Gustav Senntoneab53682018-11-01 16:30:23 +000057 private static final int MEASURE_SPEC_ANY_LENGTH =
Petr Cermak102431d2018-01-29 10:36:07 +000058 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
59
60 private static final Comparator<View> DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR =
61 (v1, v2) -> ((v2.getMeasuredWidth() - v2.getPaddingLeft() - v2.getPaddingRight())
62 - (v1.getMeasuredWidth() - v1.getPaddingLeft() - v1.getPaddingRight()));
63
64 private static final int SQUEEZE_FAILED = -1;
65
Petr Cermak10011fa2018-02-05 19:00:54 +000066 private final SmartReplyConstants mConstants;
Milo Sredkovb0f55e92018-04-04 16:13:28 +010067 private final KeyguardDismissUtil mKeyguardDismissUtil;
Milo Sredkovb2af7f92018-12-04 15:22:18 +000068 private final NotificationRemoteInputManager mRemoteInputManager;
Petr Cermak10011fa2018-02-05 19:00:54 +000069
Milo Sredkove7cf4982018-04-09 15:08:26 +010070 /**
71 * The upper bound for the height of this view in pixels. Notifications are automatically
72 * recreated on density or font size changes so caching this should be fine.
73 */
74 private final int mHeightUpperLimit;
75
Petr Cermak102431d2018-01-29 10:36:07 +000076 /** Spacing to be applied between views. */
77 private final int mSpacing;
78
79 /** Horizontal padding of smart reply buttons if all of them use only one line of text. */
80 private final int mSingleLineButtonPaddingHorizontal;
81
82 /** Horizontal padding of smart reply buttons if at least one of them uses two lines of text. */
83 private final int mDoubleLineButtonPaddingHorizontal;
84
85 /** Increase in width of a smart reply button as a result of using two lines instead of one. */
86 private final int mSingleToDoubleLineButtonWidthIncrease;
87
88 private final BreakIterator mBreakIterator;
89
90 private PriorityQueue<Button> mCandidateButtonQueueForSqueezing;
91
Kenny Guya0f6de82018-04-06 16:20:16 +010092 private View mSmartReplyContainer;
93
Gustav Senntona31f6ae2019-01-08 11:20:49 +000094 /**
95 * Whether the smart replies in this view were generated by the notification assistant. If not
96 * they're provided by the app.
97 */
98 private boolean mSmartRepliesGeneratedByAssistant = false;
99
Kenny Guy14d035c2018-05-02 19:10:36 +0100100 @ColorInt
101 private int mCurrentBackgroundColor;
102 @ColorInt
103 private final int mDefaultBackgroundColor;
104 @ColorInt
105 private final int mDefaultStrokeColor;
106 @ColorInt
107 private final int mDefaultTextColor;
108 @ColorInt
109 private final int mDefaultTextColorDarkBg;
110 @ColorInt
111 private final int mRippleColorDarkBg;
112 @ColorInt
113 private final int mRippleColor;
114 private final int mStrokeWidth;
115 private final double mMinStrokeContrast;
116
Gustav Senntoneab53682018-11-01 16:30:23 +0000117 private ActivityStarter mActivityStarter;
118
Petr Cermaked7429c2017-12-18 19:38:04 +0000119 public SmartReplyView(Context context, AttributeSet attrs) {
120 super(context, attrs);
Petr Cermak10011fa2018-02-05 19:00:54 +0000121 mConstants = Dependency.get(SmartReplyConstants.class);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100122 mKeyguardDismissUtil = Dependency.get(KeyguardDismissUtil.class);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000123 mRemoteInputManager = Dependency.get(NotificationRemoteInputManager.class);
Petr Cermak102431d2018-01-29 10:36:07 +0000124
Milo Sredkove7cf4982018-04-09 15:08:26 +0100125 mHeightUpperLimit = NotificationUtils.getFontScaledHeight(mContext,
126 R.dimen.smart_reply_button_max_height);
127
Kenny Guy14d035c2018-05-02 19:10:36 +0100128 mCurrentBackgroundColor = context.getColor(R.color.smart_reply_button_background);
129 mDefaultBackgroundColor = mCurrentBackgroundColor;
130 mDefaultTextColor = mContext.getColor(R.color.smart_reply_button_text);
131 mDefaultTextColorDarkBg = mContext.getColor(R.color.smart_reply_button_text_dark_bg);
132 mDefaultStrokeColor = mContext.getColor(R.color.smart_reply_button_stroke);
133 mRippleColor = mContext.getColor(R.color.notification_ripple_untinted_color);
134 mRippleColorDarkBg = Color.argb(Color.alpha(mRippleColor),
135 255 /* red */, 255 /* green */, 255 /* blue */);
Lucas Dupina291d192018-06-07 13:59:42 -0700136 mMinStrokeContrast = ContrastColorUtil.calculateContrast(mDefaultStrokeColor,
Kenny Guy14d035c2018-05-02 19:10:36 +0100137 mDefaultBackgroundColor);
138
Petr Cermak102431d2018-01-29 10:36:07 +0000139 int spacing = 0;
140 int singleLineButtonPaddingHorizontal = 0;
141 int doubleLineButtonPaddingHorizontal = 0;
Kenny Guy14d035c2018-05-02 19:10:36 +0100142 int strokeWidth = 0;
Petr Cermak102431d2018-01-29 10:36:07 +0000143
144 final TypedArray arr = context.obtainStyledAttributes(attrs, R.styleable.SmartReplyView,
145 0, 0);
146 final int length = arr.getIndexCount();
147 for (int i = 0; i < length; i++) {
148 int attr = arr.getIndex(i);
Jason Monk05dd5672018-08-09 09:38:21 -0400149 if (attr == R.styleable.SmartReplyView_spacing) {
150 spacing = arr.getDimensionPixelSize(i, 0);
151 } else if (attr == R.styleable.SmartReplyView_singleLineButtonPaddingHorizontal) {
152 singleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
153 } else if (attr == R.styleable.SmartReplyView_doubleLineButtonPaddingHorizontal) {
154 doubleLineButtonPaddingHorizontal = arr.getDimensionPixelSize(i, 0);
155 } else if (attr == R.styleable.SmartReplyView_buttonStrokeWidth) {
156 strokeWidth = arr.getDimensionPixelSize(i, 0);
Petr Cermak102431d2018-01-29 10:36:07 +0000157 }
158 }
159 arr.recycle();
160
Kenny Guy14d035c2018-05-02 19:10:36 +0100161 mStrokeWidth = strokeWidth;
Petr Cermak102431d2018-01-29 10:36:07 +0000162 mSpacing = spacing;
163 mSingleLineButtonPaddingHorizontal = singleLineButtonPaddingHorizontal;
164 mDoubleLineButtonPaddingHorizontal = doubleLineButtonPaddingHorizontal;
165 mSingleToDoubleLineButtonWidthIncrease =
166 2 * (doubleLineButtonPaddingHorizontal - singleLineButtonPaddingHorizontal);
167
Milo Sredkove7cf4982018-04-09 15:08:26 +0100168
Petr Cermak102431d2018-01-29 10:36:07 +0000169 mBreakIterator = BreakIterator.getLineInstance();
170 reallocateCandidateButtonQueueForSqueezing();
171 }
172
Milo Sredkove7cf4982018-04-09 15:08:26 +0100173 /**
174 * Returns an upper bound for the height of this view in pixels. This method is intended to be
175 * invoked before onMeasure, so it doesn't do any analysis on the contents of the buttons.
176 */
177 public int getHeightUpperLimit() {
178 return mHeightUpperLimit;
179 }
180
Petr Cermak102431d2018-01-29 10:36:07 +0000181 private void reallocateCandidateButtonQueueForSqueezing() {
182 // Instead of clearing the priority queue, we re-allocate so that it would fit all buttons
183 // exactly. This avoids (1) wasting memory because PriorityQueue never shrinks and
184 // (2) growing in onMeasure.
185 // The constructor throws an IllegalArgument exception if initial capacity is less than 1.
186 mCandidateButtonQueueForSqueezing = new PriorityQueue<>(
187 Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
Petr Cermaked7429c2017-12-18 19:38:04 +0000188 }
189
Gustav Senntoneab53682018-11-01 16:30:23 +0000190 /**
191 * Reset the smart suggestions view to allow adding new replies and actions.
192 */
193 public void resetSmartSuggestions(View newSmartReplyContainer) {
194 mSmartReplyContainer = newSmartReplyContainer;
Petr Cermaked7429c2017-12-18 19:38:04 +0000195 removeAllViews();
Kenny Guy14d035c2018-05-02 19:10:36 +0100196 mCurrentBackgroundColor = mDefaultBackgroundColor;
Gustav Senntoneab53682018-11-01 16:30:23 +0000197 }
198
199 /**
Gustav Sennton5759f872019-02-13 17:25:26 +0000200 * Add buttons to the {@link SmartReplyView} - these buttons must have been preinflated using
201 * one of the methods in this class.
202 */
203 public void addPreInflatedButtons(List<Button> smartSuggestionButtons) {
204 for (Button button : smartSuggestionButtons) {
205 addView(button);
206 }
207 reallocateCandidateButtonQueueForSqueezing();
208 }
209
210 /**
Gustav Senntoneab53682018-11-01 16:30:23 +0000211 * Add smart replies to this view, using the provided {@link RemoteInput} and
212 * {@link PendingIntent} to respond when the user taps a smart reply. Only the replies that fit
213 * into the notification are shown.
214 */
Gustav Sennton5759f872019-02-13 17:25:26 +0000215 public List<Button> inflateRepliesFromRemoteInput(
216 @NonNull SmartReplies smartReplies,
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100217 SmartReplyController smartReplyController, NotificationEntry entry,
218 boolean delayOnClickListener) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000219 List<Button> buttons = new ArrayList<>();
220
Tony Mak29996702018-11-26 16:23:34 +0000221 if (smartReplies.remoteInput != null && smartReplies.pendingIntent != null) {
222 if (smartReplies.choices != null) {
223 for (int i = 0; i < smartReplies.choices.length; ++i) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000224 buttons.add(inflateReplyButton(
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100225 this, getContext(), i, smartReplies, smartReplyController, entry,
226 delayOnClickListener));
Petr Cermaked7429c2017-12-18 19:38:04 +0000227 }
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000228 this.mSmartRepliesGeneratedByAssistant = smartReplies.fromAssistant;
Petr Cermaked7429c2017-12-18 19:38:04 +0000229 }
230 }
Gustav Sennton5759f872019-02-13 17:25:26 +0000231 return buttons;
Petr Cermaked7429c2017-12-18 19:38:04 +0000232 }
233
Gustav Senntoneab53682018-11-01 16:30:23 +0000234 /**
235 * Add smart actions to be shown next to smart replies. Only the actions that fit into the
236 * notification are shown.
237 */
Gustav Sennton5759f872019-02-13 17:25:26 +0000238 public List<Button> inflateSmartActions(@NonNull SmartActions smartActions,
Ned Burnsf81c4c42019-01-07 14:10:43 -0500239 SmartReplyController smartReplyController, NotificationEntry entry,
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100240 HeadsUpManager headsUpManager, boolean delayOnClickListener) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000241 List<Button> buttons = new ArrayList<>();
Tony Mak29996702018-11-26 16:23:34 +0000242 int numSmartActions = smartActions.actions.size();
Gustav Senntoneab53682018-11-01 16:30:23 +0000243 for (int n = 0; n < numSmartActions; n++) {
Tony Mak29996702018-11-26 16:23:34 +0000244 Notification.Action action = smartActions.actions.get(n);
Gustav Senntoneab53682018-11-01 16:30:23 +0000245 if (action.actionIntent != null) {
Gustav Sennton5759f872019-02-13 17:25:26 +0000246 buttons.add(inflateActionButton(
247 this, getContext(), n, smartActions, smartReplyController, entry,
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100248 headsUpManager, delayOnClickListener));
Gustav Senntoneab53682018-11-01 16:30:23 +0000249 }
250 }
Gustav Sennton5759f872019-02-13 17:25:26 +0000251 return buttons;
Gustav Senntoneab53682018-11-01 16:30:23 +0000252 }
253
Gustav Sennton5759f872019-02-13 17:25:26 +0000254 /**
255 * Inflate an instance of this class.
256 */
257 public static SmartReplyView inflate(Context context) {
258 return (SmartReplyView) LayoutInflater.from(context).inflate(
259 R.layout.smart_reply_view, null /* root */);
Petr Cermaked7429c2017-12-18 19:38:04 +0000260 }
261
Petr Cermak102431d2018-01-29 10:36:07 +0000262 @VisibleForTesting
Gustav Sennton5759f872019-02-13 17:25:26 +0000263 static Button inflateReplyButton(SmartReplyView smartReplyView, Context context,
264 int replyIndex, SmartReplies smartReplies, SmartReplyController smartReplyController,
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100265 NotificationEntry entry, boolean useDelayedOnClickListener) {
Petr Cermaked7429c2017-12-18 19:38:04 +0000266 Button b = (Button) LayoutInflater.from(context).inflate(
Gustav Sennton5759f872019-02-13 17:25:26 +0000267 R.layout.smart_reply_button, smartReplyView, false);
Tony Mak29996702018-11-26 16:23:34 +0000268 CharSequence choice = smartReplies.choices[replyIndex];
Petr Cermaked7429c2017-12-18 19:38:04 +0000269 b.setText(choice);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100270
271 OnDismissAction action = () -> {
Gustav Sennton5759f872019-02-13 17:25:26 +0000272 if (smartReplyView.mConstants.getEffectiveEditChoicesBeforeSending(
Milo Sredkov41dc4ba2018-12-27 12:03:45 +0000273 smartReplies.remoteInput.getEditChoicesBeforeSending())) {
Milo Sredkov13d88112019-02-01 12:23:24 +0000274 EditedSuggestionInfo editedSuggestionInfo =
275 new EditedSuggestionInfo(choice, replyIndex);
Gustav Sennton5759f872019-02-13 17:25:26 +0000276 smartReplyView.mRemoteInputManager.activateRemoteInput(b,
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000277 new RemoteInput[] { smartReplies.remoteInput }, smartReplies.remoteInput,
Milo Sredkov13d88112019-02-01 12:23:24 +0000278 smartReplies.pendingIntent, editedSuggestionInfo);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000279 return false;
280 }
281
Gustav Sennton13edb492019-01-28 21:40:04 +0000282 smartReplyController.smartReplySent(entry, replyIndex, b.getText(),
Milo Sredkov13d88112019-02-01 12:23:24 +0000283 NotificationLogger.getNotificationLocation(entry).toMetricsEventEnum(),
284 false /* modifiedBeforeSending */);
Petr Cermaked7429c2017-12-18 19:38:04 +0000285 Bundle results = new Bundle();
Tony Mak29996702018-11-26 16:23:34 +0000286 results.putString(smartReplies.remoteInput.getResultKey(), choice.toString());
Petr Cermaked7429c2017-12-18 19:38:04 +0000287 Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
Milo Sredkovb2af7f92018-12-04 15:22:18 +0000288 RemoteInput.addResultsToIntent(new RemoteInput[] { smartReplies.remoteInput }, intent,
Tony Mak29996702018-11-26 16:23:34 +0000289 results);
Petr Cermak9a3380c2018-01-19 15:00:24 +0000290 RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
Selim Cinekb0dc61b2018-05-22 18:49:36 -0700291 entry.setHasSentReply();
Petr Cermaked7429c2017-12-18 19:38:04 +0000292 try {
Tony Mak29996702018-11-26 16:23:34 +0000293 smartReplies.pendingIntent.send(context, 0, intent);
Petr Cermaked7429c2017-12-18 19:38:04 +0000294 } catch (PendingIntent.CanceledException e) {
295 Log.w(TAG, "Unable to send smart reply", e);
296 }
Gustav Sennton5759f872019-02-13 17:25:26 +0000297 // Note that as inflateReplyButton is called mSmartReplyContainer is null, but when the
298 // reply Button is added to the SmartReplyView mSmartReplyContainer will be set. So, it
299 // will not be possible for a user to trigger this on-click-listener without
300 // mSmartReplyContainer being set.
301 smartReplyView.mSmartReplyContainer.setVisibility(View.GONE);
Milo Sredkovb0f55e92018-04-04 16:13:28 +0100302 return false; // do not defer
303 };
304
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100305 OnClickListener onClickListener = view ->
Selim Cinekd17b3502019-07-02 20:38:32 -0700306 smartReplyView.mKeyguardDismissUtil.executeWhenUnlocked(action, !entry.isRowPinned());
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100307 if (useDelayedOnClickListener) {
308 onClickListener = new DelayedOnClickListener(onClickListener,
309 smartReplyView.mConstants.getOnClickInitDelay());
310 }
311 b.setOnClickListener(onClickListener);
Milo Sredkov66da07b2018-04-17 14:04:54 +0100312
313 b.setAccessibilityDelegate(new AccessibilityDelegate() {
314 public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
315 super.onInitializeAccessibilityNodeInfo(host, info);
Gustav Sennton5759f872019-02-13 17:25:26 +0000316 String label = smartReplyView.getResources().getString(
317 R.string.accessibility_send_smart_reply);
Milo Sredkov66da07b2018-04-17 14:04:54 +0100318 info.addAction(new AccessibilityAction(AccessibilityNodeInfo.ACTION_CLICK, label));
319 }
320 });
321
Gustav Sennton5759f872019-02-13 17:25:26 +0000322 SmartReplyView.setButtonColors(b, smartReplyView.mCurrentBackgroundColor,
323 smartReplyView.mDefaultStrokeColor, smartReplyView.mDefaultTextColor,
324 smartReplyView.mRippleColor, smartReplyView.mStrokeWidth);
Petr Cermaked7429c2017-12-18 19:38:04 +0000325 return b;
326 }
Petr Cermak102431d2018-01-29 10:36:07 +0000327
Gustav Senntoneab53682018-11-01 16:30:23 +0000328 @VisibleForTesting
Gustav Sennton5759f872019-02-13 17:25:26 +0000329 static Button inflateActionButton(SmartReplyView smartReplyView, Context context,
330 int actionIndex, SmartActions smartActions,
331 SmartReplyController smartReplyController, NotificationEntry entry,
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100332 HeadsUpManager headsUpManager, boolean useDelayedOnClickListener) {
Tony Mak7d4b3a52018-11-27 17:29:36 +0000333 Notification.Action action = smartActions.actions.get(actionIndex);
Gustav Senntoneab53682018-11-01 16:30:23 +0000334 Button button = (Button) LayoutInflater.from(context).inflate(
Gustav Sennton5759f872019-02-13 17:25:26 +0000335 R.layout.smart_action_button, smartReplyView, false);
Gustav Senntoneab53682018-11-01 16:30:23 +0000336 button.setText(action.title);
337
338 Drawable iconDrawable = action.getIcon().loadDrawable(context);
339 // Add the action icon to the Smart Action button.
Gustav Senntonc726f472018-12-04 11:57:06 +0000340 int newIconSize = context.getResources().getDimensionPixelSize(
341 R.dimen.smart_action_button_icon_size);
342 iconDrawable.setBounds(0, 0, newIconSize, newIconSize);
Gustav Senntoneab53682018-11-01 16:30:23 +0000343 button.setCompoundDrawables(iconDrawable, null, null, null);
344
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100345 OnClickListener onClickListener = view ->
Gustav Sennton5759f872019-02-13 17:25:26 +0000346 smartReplyView.getActivityStarter().startPendingIntentDismissingKeyguard(
Tony Mak7d4b3a52018-11-27 17:29:36 +0000347 action.actionIntent,
Gustav Senntond0e84532018-12-03 16:48:36 +0000348 () -> {
349 smartReplyController.smartActionClicked(
350 entry, actionIndex, action, smartActions.fromAssistant);
Gustav Sennton32137e42018-12-17 12:26:46 +0000351 headsUpManager.removeNotification(entry.key, true);
Selim Cinekab4589a2019-04-29 18:42:59 -0700352 }, entry.getRow());
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100353 if (useDelayedOnClickListener) {
354 onClickListener = new DelayedOnClickListener(onClickListener,
355 smartReplyView.mConstants.getOnClickInitDelay());
356 }
357 button.setOnClickListener(onClickListener);
Gustav Senntoneab53682018-11-01 16:30:23 +0000358
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000359 // Mark this as an Action button
360 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
361 lp.buttonType = SmartButtonType.ACTION;
Gustav Senntoneab53682018-11-01 16:30:23 +0000362 return button;
363 }
364
Petr Cermak102431d2018-01-29 10:36:07 +0000365 @Override
366 public LayoutParams generateLayoutParams(AttributeSet attrs) {
367 return new LayoutParams(mContext, attrs);
368 }
369
370 @Override
371 protected LayoutParams generateDefaultLayoutParams() {
372 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
373 }
374
375 @Override
376 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
377 return new LayoutParams(params.width, params.height);
378 }
379
380 @Override
381 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
382 final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
383 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
384
385 // Mark all buttons as hidden and un-squeezed.
386 resetButtonsLayoutParams();
387
388 if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
389 Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
390 mCandidateButtonQueueForSqueezing.clear();
391 }
392
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000393 SmartSuggestionMeasures accumulatedMeasures = new SmartSuggestionMeasures(
394 mPaddingLeft + mPaddingRight,
395 0 /* maxChildHeight */,
396 mSingleLineButtonPaddingHorizontal);
Petr Cermak102431d2018-01-29 10:36:07 +0000397 int displayedChildCount = 0;
Petr Cermak102431d2018-01-29 10:36:07 +0000398
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000399 // Set up a list of suggestions where actions come before replies. Note that the Buttons
400 // themselves have already been added to the view hierarchy in an order such that Smart
401 // Replies are shown before Smart Actions. The order of the list below determines which
402 // suggestions will be shown at all - only the first X elements are shown (where X depends
403 // on how much space each suggestion button needs).
404 List<View> smartActions = filterActionsOrReplies(SmartButtonType.ACTION);
405 List<View> smartReplies = filterActionsOrReplies(SmartButtonType.REPLY);
406 List<View> smartSuggestions = new ArrayList<>(smartActions);
407 smartSuggestions.addAll(smartReplies);
408 List<View> coveredSuggestions = new ArrayList<>();
409
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000410 // SmartSuggestionMeasures for all action buttons, this will be filled in when the first
411 // reply button is added.
412 SmartSuggestionMeasures actionsMeasures = null;
413
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000414 final int maxNumActions = mConstants.getMaxNumActions();
415 int numShownActions = 0;
416
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000417 for (View child : smartSuggestions) {
Petr Cermak102431d2018-01-29 10:36:07 +0000418 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000419 if (maxNumActions != -1 // -1 means 'no limit'
420 && lp.buttonType == SmartButtonType.ACTION
421 && numShownActions >= maxNumActions) {
422 // We've reached the maximum number of actions, don't add another one!
423 continue;
424 }
Petr Cermak102431d2018-01-29 10:36:07 +0000425
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000426 child.setPadding(accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingTop(),
427 accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingBottom());
Gustav Senntoneab53682018-11-01 16:30:23 +0000428 child.measure(MEASURE_SPEC_ANY_LENGTH, heightMeasureSpec);
Petr Cermak102431d2018-01-29 10:36:07 +0000429
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000430 coveredSuggestions.add(child);
431
Petr Cermak102431d2018-01-29 10:36:07 +0000432 final int lineCount = ((Button) child).getLineCount();
433 if (lineCount < 1 || lineCount > 2) {
434 // If smart reply has no text, or more than two lines, then don't show it.
435 continue;
436 }
437
438 if (lineCount == 1) {
439 mCandidateButtonQueueForSqueezing.add((Button) child);
440 }
441
442 // Remember the current measurements in case the current button doesn't fit in.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000443 SmartSuggestionMeasures originalMeasures = accumulatedMeasures.clone();
444 if (actionsMeasures == null && lp.buttonType == SmartButtonType.REPLY) {
445 // We've added all actions (we go through actions first), now add their
446 // measurements.
447 actionsMeasures = accumulatedMeasures.clone();
448 }
Petr Cermak102431d2018-01-29 10:36:07 +0000449
450 final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
451 final int childWidth = child.getMeasuredWidth();
452 final int childHeight = child.getMeasuredHeight();
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000453 accumulatedMeasures.mMeasuredWidth += spacing + childWidth;
454 accumulatedMeasures.mMaxChildHeight =
455 Math.max(accumulatedMeasures.mMaxChildHeight, childHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000456
457 // Do we need to increase the number of lines in smart reply buttons to two?
458 final boolean increaseToTwoLines =
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000459 (accumulatedMeasures.mButtonPaddingHorizontal
460 == mSingleLineButtonPaddingHorizontal)
461 && (lineCount == 2 || accumulatedMeasures.mMeasuredWidth > targetWidth);
Petr Cermak102431d2018-01-29 10:36:07 +0000462 if (increaseToTwoLines) {
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000463 accumulatedMeasures.mMeasuredWidth +=
464 (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease;
465 accumulatedMeasures.mButtonPaddingHorizontal =
466 mDoubleLineButtonPaddingHorizontal;
Petr Cermak102431d2018-01-29 10:36:07 +0000467 }
468
469 // If the last button doesn't fit into the remaining width, try squeezing preceding
470 // smart reply buttons.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000471 if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
Petr Cermak102431d2018-01-29 10:36:07 +0000472 // Keep squeezing preceding and current smart reply buttons until they all fit.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000473 while (accumulatedMeasures.mMeasuredWidth > targetWidth
Petr Cermak102431d2018-01-29 10:36:07 +0000474 && !mCandidateButtonQueueForSqueezing.isEmpty()) {
475 final Button candidate = mCandidateButtonQueueForSqueezing.poll();
476 final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
477 if (squeezeReduction != SQUEEZE_FAILED) {
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000478 accumulatedMeasures.mMaxChildHeight =
479 Math.max(accumulatedMeasures.mMaxChildHeight,
480 candidate.getMeasuredHeight());
481 accumulatedMeasures.mMeasuredWidth -= squeezeReduction;
Petr Cermak102431d2018-01-29 10:36:07 +0000482 }
483 }
484
485 // If the current button still doesn't fit after squeezing all buttons, undo the
486 // last squeezing round.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000487 if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
488 accumulatedMeasures = originalMeasures;
Petr Cermak102431d2018-01-29 10:36:07 +0000489
490 // Mark all buttons from the last squeezing round as "failed to squeeze", so
491 // that they're re-measured without squeezing later.
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000492 markButtonsWithPendingSqueezeStatusAs(
493 LayoutParams.SQUEEZE_STATUS_FAILED, coveredSuggestions);
Petr Cermak102431d2018-01-29 10:36:07 +0000494
Gustav Sennton35156752018-12-20 10:35:03 +0000495 // The current button doesn't fit, keep on adding lower-priority buttons in case
496 // any of those fit.
497 continue;
Petr Cermak102431d2018-01-29 10:36:07 +0000498 }
499
500 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
501 // to prevent them from being un-squeezed in a subsequent squeezing round.
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000502 markButtonsWithPendingSqueezeStatusAs(
503 LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, coveredSuggestions);
Petr Cermak102431d2018-01-29 10:36:07 +0000504 }
505
506 lp.show = true;
507 displayedChildCount++;
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000508 if (lp.buttonType == SmartButtonType.ACTION) {
509 numShownActions++;
510 }
Petr Cermak102431d2018-01-29 10:36:07 +0000511 }
512
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000513 if (mSmartRepliesGeneratedByAssistant) {
514 if (!gotEnoughSmartReplies(smartReplies)) {
515 // We don't have enough smart replies - hide all of them.
516 for (View smartReplyButton : smartReplies) {
517 final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
518 lp.show = false;
519 }
520 // Reset our measures back to when we had only added actions (before adding
521 // replies).
522 accumulatedMeasures = actionsMeasures;
523 }
524 }
525
Petr Cermak102431d2018-01-29 10:36:07 +0000526 // We're done squeezing buttons, so we can clear the priority queue.
527 mCandidateButtonQueueForSqueezing.clear();
528
Milo Sredkova5bacea2018-04-12 12:52:43 +0100529 // Finally, we need to re-measure some buttons.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000530 remeasureButtonsIfNecessary(accumulatedMeasures.mButtonPaddingHorizontal,
531 accumulatedMeasures.mMaxChildHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000532
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000533 int buttonHeight = Math.max(getSuggestedMinimumHeight(), mPaddingTop
534 + accumulatedMeasures.mMaxChildHeight + mPaddingBottom);
535
536 // Set the corner radius to half the button height to make the side of the buttons look like
537 // a semicircle.
538 for (View smartSuggestionButton : smartSuggestions) {
539 setCornerRadius((Button) smartSuggestionButton, ((float) buttonHeight) / 2);
540 }
541
Petr Cermak102431d2018-01-29 10:36:07 +0000542 setMeasuredDimension(
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000543 resolveSize(Math.max(getSuggestedMinimumWidth(),
544 accumulatedMeasures.mMeasuredWidth),
545 widthMeasureSpec),
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000546 resolveSize(buttonHeight, heightMeasureSpec));
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000547 }
548
549 /**
550 * Fields we keep track of inside onMeasure() to correctly measure the SmartReplyView depending
551 * on which suggestions are added.
552 */
553 private static class SmartSuggestionMeasures {
554 int mMeasuredWidth = -1;
555 int mMaxChildHeight = -1;
556 int mButtonPaddingHorizontal = -1;
557
558 SmartSuggestionMeasures(int measuredWidth, int maxChildHeight,
559 int buttonPaddingHorizontal) {
560 this.mMeasuredWidth = measuredWidth;
561 this.mMaxChildHeight = maxChildHeight;
562 this.mButtonPaddingHorizontal = buttonPaddingHorizontal;
563 }
564
565 public SmartSuggestionMeasures clone() {
566 return new SmartSuggestionMeasures(
567 mMeasuredWidth, mMaxChildHeight, mButtonPaddingHorizontal);
568 }
569 }
570
571 /**
572 * Returns whether our notification contains at least N smart replies (or 0) where N is
573 * determined by {@link SmartReplyConstants}.
574 */
575 private boolean gotEnoughSmartReplies(List<View> smartReplies) {
576 int numShownReplies = 0;
577 for (View smartReplyButton : smartReplies) {
578 final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
579 if (lp.show) {
580 numShownReplies++;
581 }
582 }
583 if (numShownReplies == 0
584 || numShownReplies >= mConstants.getMinNumSystemGeneratedReplies()) {
585 // We have enough replies, yay!
586 return true;
587 }
588 return false;
Petr Cermak102431d2018-01-29 10:36:07 +0000589 }
590
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000591 private List<View> filterActionsOrReplies(SmartButtonType buttonType) {
592 List<View> actions = new ArrayList<>();
593 final int childCount = getChildCount();
594 for (int i = 0; i < childCount; i++) {
595 final View child = getChildAt(i);
596 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
597 if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
598 continue;
599 }
600 if (lp.buttonType == buttonType) {
601 actions.add(child);
602 }
603 }
604 return actions;
605 }
606
Petr Cermak102431d2018-01-29 10:36:07 +0000607 private void resetButtonsLayoutParams() {
608 final int childCount = getChildCount();
609 for (int i = 0; i < childCount; i++) {
610 final View child = getChildAt(i);
611 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
612 lp.show = false;
613 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
614 }
615 }
616
617 private int squeezeButton(Button button, int heightMeasureSpec) {
618 final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
619 if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
620 return SQUEEZE_FAILED;
621 }
622 return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
623 }
624
625 private int estimateOptimalSqueezedButtonTextWidth(Button button) {
626 // Find a line-break point in the middle of the smart reply button text.
627 final String rawText = button.getText().toString();
628
629 // The button sometimes has a transformation affecting text layout (e.g. all caps).
630 final TransformationMethod transformation = button.getTransformationMethod();
631 final String text = transformation == null ?
632 rawText : transformation.getTransformation(rawText, button).toString();
633 final int length = text.length();
634 mBreakIterator.setText(text);
635
636 if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
637 if (mBreakIterator.next() == BreakIterator.DONE) {
638 // Can't find a single possible line break in either direction.
639 return SQUEEZE_FAILED;
640 }
641 }
642
643 final TextPaint paint = button.getPaint();
644 final int initialPosition = mBreakIterator.current();
645 final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
646 final float initialRightTextWidth =
647 Layout.getDesiredWidth(text, initialPosition, length, paint);
648 float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
649
650 if (initialLeftTextWidth != initialRightTextWidth) {
651 // See if there's a better line-break point (leading to a more narrow button) in
652 // either left or right direction.
653 final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
654 final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
655 for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
656 final int newPosition =
657 moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
658 if (newPosition == BreakIterator.DONE) {
659 break;
660 }
661
662 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
663 final float newRightTextWidth =
664 Layout.getDesiredWidth(text, newPosition, length, paint);
665 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
666 if (newOptimalTextWidth < optimalTextWidth) {
667 optimalTextWidth = newOptimalTextWidth;
668 } else {
669 break;
670 }
671
672 boolean tooFar = moveLeft
673 ? newLeftTextWidth <= newRightTextWidth
674 : newLeftTextWidth >= newRightTextWidth;
675 if (tooFar) {
676 break;
677 }
678 }
679 }
680
681 return (int) Math.ceil(optimalTextWidth);
682 }
683
Gustav Senntoneab53682018-11-01 16:30:23 +0000684 /**
685 * Returns the combined width of the left drawable (the action icon) and the padding between the
686 * drawable and the button text.
687 */
688 private int getLeftCompoundDrawableWidthWithPadding(Button button) {
689 Drawable[] drawables = button.getCompoundDrawables();
690 Drawable leftDrawable = drawables[0];
691 if (leftDrawable == null) return 0;
692
693 return leftDrawable.getBounds().width() + button.getCompoundDrawablePadding();
694 }
695
Petr Cermak102431d2018-01-29 10:36:07 +0000696 private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
697 int oldWidth = button.getMeasuredWidth();
698 if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
699 // Correct for the fact that the button was laid out with single-line horizontal
700 // padding.
701 oldWidth += mSingleToDoubleLineButtonWidthIncrease;
702 }
703
704 // Re-measure the squeezed smart reply button.
705 button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
706 mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
707 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
Gustav Senntoneab53682018-11-01 16:30:23 +0000708 2 * mDoubleLineButtonPaddingHorizontal + textWidth
709 + getLeftCompoundDrawableWidthWithPadding(button), MeasureSpec.AT_MOST);
Petr Cermak102431d2018-01-29 10:36:07 +0000710 button.measure(widthMeasureSpec, heightMeasureSpec);
711
712 final int newWidth = button.getMeasuredWidth();
713
714 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
715 if (button.getLineCount() > 2 || newWidth >= oldWidth) {
716 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
717 return SQUEEZE_FAILED;
718 } else {
719 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
720 return oldWidth - newWidth;
721 }
722 }
723
Milo Sredkova5bacea2018-04-12 12:52:43 +0100724 private void remeasureButtonsIfNecessary(
Petr Cermak102431d2018-01-29 10:36:07 +0000725 int buttonPaddingHorizontal, int maxChildHeight) {
Petr Cermak102431d2018-01-29 10:36:07 +0000726 final int maxChildHeightMeasure =
727 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
728
729 final int childCount = getChildCount();
730 for (int i = 0; i < childCount; i++) {
731 final View child = getChildAt(i);
732 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
733 if (!lp.show) {
734 continue;
735 }
736
Petr Cermak102431d2018-01-29 10:36:07 +0000737 boolean requiresNewMeasure = false;
738 int newWidth = child.getMeasuredWidth();
739
740 // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
741 // in more than two lines or because it was unnecessary).
742 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
743 requiresNewMeasure = true;
744 newWidth = Integer.MAX_VALUE;
745 }
746
747 // Re-measure reason 2: The button's horizontal padding is incorrect (because it was
748 // measured with the wrong number of lines).
749 if (child.getPaddingLeft() != buttonPaddingHorizontal) {
750 requiresNewMeasure = true;
Kenny Guy48ee6d62018-05-09 16:51:26 +0100751 if (newWidth != Integer.MAX_VALUE) {
752 if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) {
753 // Change padding (2->1 line).
754 newWidth -= mSingleToDoubleLineButtonWidthIncrease;
755 } else {
756 // Change padding (1->2 lines).
757 newWidth += mSingleToDoubleLineButtonWidthIncrease;
758 }
Petr Cermak102431d2018-01-29 10:36:07 +0000759 }
760 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
761 buttonPaddingHorizontal, child.getPaddingBottom());
762 }
763
764 // Re-measure reason 3: The button's height is less than the max height of all buttons
765 // (all should have the same height).
766 if (child.getMeasuredHeight() != maxChildHeight) {
767 requiresNewMeasure = true;
768 }
769
770 if (requiresNewMeasure) {
771 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
772 maxChildHeightMeasure);
773 }
774 }
775 }
776
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000777 private void markButtonsWithPendingSqueezeStatusAs(
778 int squeezeStatus, List<View> coveredChildren) {
779 for (View child : coveredChildren) {
Petr Cermak102431d2018-01-29 10:36:07 +0000780 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
781 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
782 lp.squeezeStatus = squeezeStatus;
783 }
784 }
785 }
786
787 @Override
788 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
789 final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
790
791 final int width = right - left;
792 int position = isRtl ? width - mPaddingRight : mPaddingLeft;
793
794 final int childCount = getChildCount();
795 for (int i = 0; i < childCount; i++) {
796 final View child = getChildAt(i);
797 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
798 if (!lp.show) {
799 continue;
800 }
801
802 final int childWidth = child.getMeasuredWidth();
803 final int childHeight = child.getMeasuredHeight();
804 final int childLeft = isRtl ? position - childWidth : position;
805 child.layout(childLeft, 0, childLeft + childWidth, childHeight);
806
807 final int childWidthWithSpacing = childWidth + mSpacing;
808 if (isRtl) {
809 position -= childWidthWithSpacing;
810 } else {
811 position += childWidthWithSpacing;
812 }
813 }
814 }
815
816 @Override
817 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
818 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
819 return lp.show && super.drawChild(canvas, child, drawingTime);
820 }
821
Kenny Guy14d035c2018-05-02 19:10:36 +0100822 public void setBackgroundTintColor(int backgroundColor) {
823 if (backgroundColor == mCurrentBackgroundColor) {
824 // Same color ignoring.
825 return;
826 }
827 mCurrentBackgroundColor = backgroundColor;
828
Lucas Dupina291d192018-06-07 13:59:42 -0700829 final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100830
Lucas Dupina291d192018-06-07 13:59:42 -0700831 int textColor = ContrastColorUtil.ensureTextContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100832 dark ? mDefaultTextColorDarkBg : mDefaultTextColor,
833 backgroundColor | 0xff000000, dark);
Lucas Dupina291d192018-06-07 13:59:42 -0700834 int strokeColor = ContrastColorUtil.ensureContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100835 mDefaultStrokeColor, backgroundColor | 0xff000000, dark, mMinStrokeContrast);
836 int rippleColor = dark ? mRippleColorDarkBg : mRippleColor;
837
838 int childCount = getChildCount();
839 for (int i = 0; i < childCount; i++) {
840 final Button child = (Button) getChildAt(i);
Gustav Sennton5759f872019-02-13 17:25:26 +0000841 setButtonColors(child, backgroundColor, strokeColor, textColor, rippleColor,
842 mStrokeWidth);
Kenny Guy14d035c2018-05-02 19:10:36 +0100843 }
844 }
845
Gustav Sennton5759f872019-02-13 17:25:26 +0000846 private static void setButtonColors(Button button, int backgroundColor, int strokeColor,
847 int textColor, int rippleColor, int strokeWidth) {
Kenny Guy14d035c2018-05-02 19:10:36 +0100848 Drawable drawable = button.getBackground();
849 if (drawable instanceof RippleDrawable) {
850 // Mutate in case other notifications are using this drawable.
851 drawable = drawable.mutate();
852 RippleDrawable ripple = (RippleDrawable) drawable;
853 ripple.setColor(ColorStateList.valueOf(rippleColor));
854 Drawable inset = ripple.getDrawable(0);
855 if (inset instanceof InsetDrawable) {
856 Drawable background = ((InsetDrawable) inset).getDrawable();
857 if (background instanceof GradientDrawable) {
858 GradientDrawable gradientDrawable = (GradientDrawable) background;
859 gradientDrawable.setColor(backgroundColor);
Gustav Sennton5759f872019-02-13 17:25:26 +0000860 gradientDrawable.setStroke(strokeWidth, strokeColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100861 }
862 }
863 button.setBackground(drawable);
864 }
865 button.setTextColor(textColor);
866 }
867
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000868 private void setCornerRadius(Button button, float radius) {
869 Drawable drawable = button.getBackground();
870 if (drawable instanceof RippleDrawable) {
871 // Mutate in case other notifications are using this drawable.
872 drawable = drawable.mutate();
873 RippleDrawable ripple = (RippleDrawable) drawable;
874 Drawable inset = ripple.getDrawable(0);
875 if (inset instanceof InsetDrawable) {
876 Drawable background = ((InsetDrawable) inset).getDrawable();
877 if (background instanceof GradientDrawable) {
878 GradientDrawable gradientDrawable = (GradientDrawable) background;
879 gradientDrawable.setCornerRadius(radius);
880 }
881 }
882 }
883 }
884
Gustav Senntoneab53682018-11-01 16:30:23 +0000885 private ActivityStarter getActivityStarter() {
886 if (mActivityStarter == null) {
887 mActivityStarter = Dependency.get(ActivityStarter.class);
888 }
889 return mActivityStarter;
890 }
891
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000892 private enum SmartButtonType {
893 REPLY,
894 ACTION
895 }
896
Petr Cermak102431d2018-01-29 10:36:07 +0000897 @VisibleForTesting
898 static class LayoutParams extends ViewGroup.LayoutParams {
899
900 /** Button is not squeezed. */
901 private static final int SQUEEZE_STATUS_NONE = 0;
902
903 /**
904 * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
905 * turns out to have been unnecessary (because there's still not enough space to add another
906 * button).
907 */
908 private static final int SQUEEZE_STATUS_PENDING = 1;
909
910 /** Button was successfully squeezed and it won't be un-squeezed. */
911 private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
912
913 /**
914 * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
915 * text or it didn't reduce the button's width at all. The button will have to be
916 * re-measured to use only one line of text.
917 */
918 private static final int SQUEEZE_STATUS_FAILED = 3;
919
920 private boolean show = false;
921 private int squeezeStatus = SQUEEZE_STATUS_NONE;
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000922 private SmartButtonType buttonType = SmartButtonType.REPLY;
Petr Cermak102431d2018-01-29 10:36:07 +0000923
924 private LayoutParams(Context c, AttributeSet attrs) {
925 super(c, attrs);
926 }
927
928 private LayoutParams(int width, int height) {
929 super(width, height);
930 }
931
932 @VisibleForTesting
933 boolean isShown() {
934 return show;
935 }
936 }
Tony Mak29996702018-11-26 16:23:34 +0000937
938 /**
939 * Data class for smart replies.
940 */
941 public static class SmartReplies {
942 @NonNull
943 public final RemoteInput remoteInput;
944 @NonNull
945 public final PendingIntent pendingIntent;
946 @NonNull
947 public final CharSequence[] choices;
948 public final boolean fromAssistant;
949
950 public SmartReplies(CharSequence[] choices, RemoteInput remoteInput,
951 PendingIntent pendingIntent, boolean fromAssistant) {
952 this.choices = choices;
953 this.remoteInput = remoteInput;
954 this.pendingIntent = pendingIntent;
955 this.fromAssistant = fromAssistant;
956 }
957 }
958
959
960 /**
961 * Data class for smart actions.
962 */
963 public static class SmartActions {
964 @NonNull
965 public final List<Notification.Action> actions;
966 public final boolean fromAssistant;
967
968 public SmartActions(List<Notification.Action> actions, boolean fromAssistant) {
969 this.actions = actions;
970 this.fromAssistant = fromAssistant;
971 }
972 }
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100973
974 /**
975 * An OnClickListener wrapper that blocks the underlying OnClickListener for a given amount of
976 * time.
977 */
978 private static class DelayedOnClickListener implements OnClickListener {
979 private final OnClickListener mActualListener;
980 private final long mInitDelayMs;
981 private final long mInitTimeMs;
982
983 DelayedOnClickListener(OnClickListener actualOnClickListener, long initDelayMs) {
984 mActualListener = actualOnClickListener;
985 mInitDelayMs = initDelayMs;
986 mInitTimeMs = SystemClock.elapsedRealtime();
987 }
988
989 public void onClick(View v) {
990 if (hasFinishedInitialization()) {
991 mActualListener.onClick(v);
992 } else {
993 Log.i(TAG, "Accidental Smart Suggestion click registered, delay: " + mInitDelayMs);
994 }
995 }
996
997 private boolean hasFinishedInitialization() {
998 return SystemClock.elapsedRealtime() >= mInitTimeMs + mInitDelayMs;
999 }
1000 }
Petr Cermaked7429c2017-12-18 19:38:04 +00001001}