blob: ed0b9d9294660d5f9dad7232b206cad0aa325bf8 [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 Senntondfa968d2019-09-13 12:00:50 +0100238 public List<Button> inflateSmartActions(Context packageContext,
239 @NonNull SmartActions smartActions, SmartReplyController smartReplyController,
240 NotificationEntry entry, 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(
Gustav Senntondfa968d2019-09-13 12:00:50 +0100247 this, getContext(), packageContext, n, smartActions, smartReplyController,
248 entry, 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,
Gustav Senntondfa968d2019-09-13 12:00:50 +0100330 Context packageContext, int actionIndex, SmartActions smartActions,
Gustav Sennton5759f872019-02-13 17:25:26 +0000331 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
Gustav Senntondfa968d2019-09-13 12:00:50 +0100338 // We received the Icon from the application - so use the Context of the application to
339 // reference icon resources.
340 Drawable iconDrawable = action.getIcon().loadDrawable(packageContext);
Gustav Senntoneab53682018-11-01 16:30:23 +0000341 // Add the action icon to the Smart Action button.
Gustav Senntonc726f472018-12-04 11:57:06 +0000342 int newIconSize = context.getResources().getDimensionPixelSize(
343 R.dimen.smart_action_button_icon_size);
344 iconDrawable.setBounds(0, 0, newIconSize, newIconSize);
Gustav Senntoneab53682018-11-01 16:30:23 +0000345 button.setCompoundDrawables(iconDrawable, null, null, null);
346
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100347 OnClickListener onClickListener = view ->
Gustav Sennton5759f872019-02-13 17:25:26 +0000348 smartReplyView.getActivityStarter().startPendingIntentDismissingKeyguard(
Tony Mak7d4b3a52018-11-27 17:29:36 +0000349 action.actionIntent,
Gustav Senntond0e84532018-12-03 16:48:36 +0000350 () -> {
351 smartReplyController.smartActionClicked(
352 entry, actionIndex, action, smartActions.fromAssistant);
Gustav Sennton32137e42018-12-17 12:26:46 +0000353 headsUpManager.removeNotification(entry.key, true);
Selim Cinekab4589a2019-04-29 18:42:59 -0700354 }, entry.getRow());
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100355 if (useDelayedOnClickListener) {
356 onClickListener = new DelayedOnClickListener(onClickListener,
357 smartReplyView.mConstants.getOnClickInitDelay());
358 }
359 button.setOnClickListener(onClickListener);
Gustav Senntoneab53682018-11-01 16:30:23 +0000360
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000361 // Mark this as an Action button
362 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
363 lp.buttonType = SmartButtonType.ACTION;
Gustav Senntoneab53682018-11-01 16:30:23 +0000364 return button;
365 }
366
Petr Cermak102431d2018-01-29 10:36:07 +0000367 @Override
368 public LayoutParams generateLayoutParams(AttributeSet attrs) {
369 return new LayoutParams(mContext, attrs);
370 }
371
372 @Override
373 protected LayoutParams generateDefaultLayoutParams() {
374 return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
375 }
376
377 @Override
378 protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams params) {
379 return new LayoutParams(params.width, params.height);
380 }
381
382 @Override
383 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
384 final int targetWidth = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED
385 ? Integer.MAX_VALUE : MeasureSpec.getSize(widthMeasureSpec);
386
387 // Mark all buttons as hidden and un-squeezed.
388 resetButtonsLayoutParams();
389
390 if (!mCandidateButtonQueueForSqueezing.isEmpty()) {
391 Log.wtf(TAG, "Single line button queue leaked between onMeasure calls");
392 mCandidateButtonQueueForSqueezing.clear();
393 }
394
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000395 SmartSuggestionMeasures accumulatedMeasures = new SmartSuggestionMeasures(
396 mPaddingLeft + mPaddingRight,
397 0 /* maxChildHeight */,
398 mSingleLineButtonPaddingHorizontal);
Petr Cermak102431d2018-01-29 10:36:07 +0000399 int displayedChildCount = 0;
Petr Cermak102431d2018-01-29 10:36:07 +0000400
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000401 // Set up a list of suggestions where actions come before replies. Note that the Buttons
402 // themselves have already been added to the view hierarchy in an order such that Smart
403 // Replies are shown before Smart Actions. The order of the list below determines which
404 // suggestions will be shown at all - only the first X elements are shown (where X depends
405 // on how much space each suggestion button needs).
406 List<View> smartActions = filterActionsOrReplies(SmartButtonType.ACTION);
407 List<View> smartReplies = filterActionsOrReplies(SmartButtonType.REPLY);
408 List<View> smartSuggestions = new ArrayList<>(smartActions);
409 smartSuggestions.addAll(smartReplies);
410 List<View> coveredSuggestions = new ArrayList<>();
411
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000412 // SmartSuggestionMeasures for all action buttons, this will be filled in when the first
413 // reply button is added.
414 SmartSuggestionMeasures actionsMeasures = null;
415
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000416 final int maxNumActions = mConstants.getMaxNumActions();
417 int numShownActions = 0;
418
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000419 for (View child : smartSuggestions) {
Petr Cermak102431d2018-01-29 10:36:07 +0000420 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000421 if (maxNumActions != -1 // -1 means 'no limit'
422 && lp.buttonType == SmartButtonType.ACTION
423 && numShownActions >= maxNumActions) {
424 // We've reached the maximum number of actions, don't add another one!
425 continue;
426 }
Petr Cermak102431d2018-01-29 10:36:07 +0000427
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000428 child.setPadding(accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingTop(),
429 accumulatedMeasures.mButtonPaddingHorizontal, child.getPaddingBottom());
Gustav Senntoneab53682018-11-01 16:30:23 +0000430 child.measure(MEASURE_SPEC_ANY_LENGTH, heightMeasureSpec);
Petr Cermak102431d2018-01-29 10:36:07 +0000431
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000432 coveredSuggestions.add(child);
433
Petr Cermak102431d2018-01-29 10:36:07 +0000434 final int lineCount = ((Button) child).getLineCount();
435 if (lineCount < 1 || lineCount > 2) {
436 // If smart reply has no text, or more than two lines, then don't show it.
437 continue;
438 }
439
440 if (lineCount == 1) {
441 mCandidateButtonQueueForSqueezing.add((Button) child);
442 }
443
444 // Remember the current measurements in case the current button doesn't fit in.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000445 SmartSuggestionMeasures originalMeasures = accumulatedMeasures.clone();
446 if (actionsMeasures == null && lp.buttonType == SmartButtonType.REPLY) {
447 // We've added all actions (we go through actions first), now add their
448 // measurements.
449 actionsMeasures = accumulatedMeasures.clone();
450 }
Petr Cermak102431d2018-01-29 10:36:07 +0000451
452 final int spacing = displayedChildCount == 0 ? 0 : mSpacing;
453 final int childWidth = child.getMeasuredWidth();
454 final int childHeight = child.getMeasuredHeight();
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000455 accumulatedMeasures.mMeasuredWidth += spacing + childWidth;
456 accumulatedMeasures.mMaxChildHeight =
457 Math.max(accumulatedMeasures.mMaxChildHeight, childHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000458
459 // Do we need to increase the number of lines in smart reply buttons to two?
460 final boolean increaseToTwoLines =
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000461 (accumulatedMeasures.mButtonPaddingHorizontal
462 == mSingleLineButtonPaddingHorizontal)
463 && (lineCount == 2 || accumulatedMeasures.mMeasuredWidth > targetWidth);
Petr Cermak102431d2018-01-29 10:36:07 +0000464 if (increaseToTwoLines) {
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000465 accumulatedMeasures.mMeasuredWidth +=
466 (displayedChildCount + 1) * mSingleToDoubleLineButtonWidthIncrease;
467 accumulatedMeasures.mButtonPaddingHorizontal =
468 mDoubleLineButtonPaddingHorizontal;
Petr Cermak102431d2018-01-29 10:36:07 +0000469 }
470
471 // If the last button doesn't fit into the remaining width, try squeezing preceding
472 // smart reply buttons.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000473 if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
Petr Cermak102431d2018-01-29 10:36:07 +0000474 // Keep squeezing preceding and current smart reply buttons until they all fit.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000475 while (accumulatedMeasures.mMeasuredWidth > targetWidth
Petr Cermak102431d2018-01-29 10:36:07 +0000476 && !mCandidateButtonQueueForSqueezing.isEmpty()) {
477 final Button candidate = mCandidateButtonQueueForSqueezing.poll();
478 final int squeezeReduction = squeezeButton(candidate, heightMeasureSpec);
479 if (squeezeReduction != SQUEEZE_FAILED) {
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000480 accumulatedMeasures.mMaxChildHeight =
481 Math.max(accumulatedMeasures.mMaxChildHeight,
482 candidate.getMeasuredHeight());
483 accumulatedMeasures.mMeasuredWidth -= squeezeReduction;
Petr Cermak102431d2018-01-29 10:36:07 +0000484 }
485 }
486
487 // If the current button still doesn't fit after squeezing all buttons, undo the
488 // last squeezing round.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000489 if (accumulatedMeasures.mMeasuredWidth > targetWidth) {
490 accumulatedMeasures = originalMeasures;
Petr Cermak102431d2018-01-29 10:36:07 +0000491
492 // Mark all buttons from the last squeezing round as "failed to squeeze", so
493 // that they're re-measured without squeezing later.
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000494 markButtonsWithPendingSqueezeStatusAs(
495 LayoutParams.SQUEEZE_STATUS_FAILED, coveredSuggestions);
Petr Cermak102431d2018-01-29 10:36:07 +0000496
Gustav Sennton35156752018-12-20 10:35:03 +0000497 // The current button doesn't fit, keep on adding lower-priority buttons in case
498 // any of those fit.
499 continue;
Petr Cermak102431d2018-01-29 10:36:07 +0000500 }
501
502 // The current button fits, so mark all squeezed buttons as "successfully squeezed"
503 // to prevent them from being un-squeezed in a subsequent squeezing round.
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000504 markButtonsWithPendingSqueezeStatusAs(
505 LayoutParams.SQUEEZE_STATUS_SUCCESSFUL, coveredSuggestions);
Petr Cermak102431d2018-01-29 10:36:07 +0000506 }
507
508 lp.show = true;
509 displayedChildCount++;
Gustav Sennton4bf5ff52019-01-16 14:27:25 +0000510 if (lp.buttonType == SmartButtonType.ACTION) {
511 numShownActions++;
512 }
Petr Cermak102431d2018-01-29 10:36:07 +0000513 }
514
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000515 if (mSmartRepliesGeneratedByAssistant) {
516 if (!gotEnoughSmartReplies(smartReplies)) {
517 // We don't have enough smart replies - hide all of them.
518 for (View smartReplyButton : smartReplies) {
519 final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
520 lp.show = false;
521 }
522 // Reset our measures back to when we had only added actions (before adding
523 // replies).
524 accumulatedMeasures = actionsMeasures;
525 }
526 }
527
Petr Cermak102431d2018-01-29 10:36:07 +0000528 // We're done squeezing buttons, so we can clear the priority queue.
529 mCandidateButtonQueueForSqueezing.clear();
530
Milo Sredkova5bacea2018-04-12 12:52:43 +0100531 // Finally, we need to re-measure some buttons.
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000532 remeasureButtonsIfNecessary(accumulatedMeasures.mButtonPaddingHorizontal,
533 accumulatedMeasures.mMaxChildHeight);
Petr Cermak102431d2018-01-29 10:36:07 +0000534
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000535 int buttonHeight = Math.max(getSuggestedMinimumHeight(), mPaddingTop
536 + accumulatedMeasures.mMaxChildHeight + mPaddingBottom);
537
538 // Set the corner radius to half the button height to make the side of the buttons look like
539 // a semicircle.
540 for (View smartSuggestionButton : smartSuggestions) {
541 setCornerRadius((Button) smartSuggestionButton, ((float) buttonHeight) / 2);
542 }
543
Petr Cermak102431d2018-01-29 10:36:07 +0000544 setMeasuredDimension(
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000545 resolveSize(Math.max(getSuggestedMinimumWidth(),
546 accumulatedMeasures.mMeasuredWidth),
547 widthMeasureSpec),
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000548 resolveSize(buttonHeight, heightMeasureSpec));
Gustav Senntona31f6ae2019-01-08 11:20:49 +0000549 }
550
551 /**
552 * Fields we keep track of inside onMeasure() to correctly measure the SmartReplyView depending
553 * on which suggestions are added.
554 */
555 private static class SmartSuggestionMeasures {
556 int mMeasuredWidth = -1;
557 int mMaxChildHeight = -1;
558 int mButtonPaddingHorizontal = -1;
559
560 SmartSuggestionMeasures(int measuredWidth, int maxChildHeight,
561 int buttonPaddingHorizontal) {
562 this.mMeasuredWidth = measuredWidth;
563 this.mMaxChildHeight = maxChildHeight;
564 this.mButtonPaddingHorizontal = buttonPaddingHorizontal;
565 }
566
567 public SmartSuggestionMeasures clone() {
568 return new SmartSuggestionMeasures(
569 mMeasuredWidth, mMaxChildHeight, mButtonPaddingHorizontal);
570 }
571 }
572
573 /**
574 * Returns whether our notification contains at least N smart replies (or 0) where N is
575 * determined by {@link SmartReplyConstants}.
576 */
577 private boolean gotEnoughSmartReplies(List<View> smartReplies) {
578 int numShownReplies = 0;
579 for (View smartReplyButton : smartReplies) {
580 final LayoutParams lp = (LayoutParams) smartReplyButton.getLayoutParams();
581 if (lp.show) {
582 numShownReplies++;
583 }
584 }
585 if (numShownReplies == 0
586 || numShownReplies >= mConstants.getMinNumSystemGeneratedReplies()) {
587 // We have enough replies, yay!
588 return true;
589 }
590 return false;
Petr Cermak102431d2018-01-29 10:36:07 +0000591 }
592
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000593 private List<View> filterActionsOrReplies(SmartButtonType buttonType) {
594 List<View> actions = new ArrayList<>();
595 final int childCount = getChildCount();
596 for (int i = 0; i < childCount; i++) {
597 final View child = getChildAt(i);
598 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
599 if (child.getVisibility() != View.VISIBLE || !(child instanceof Button)) {
600 continue;
601 }
602 if (lp.buttonType == buttonType) {
603 actions.add(child);
604 }
605 }
606 return actions;
607 }
608
Petr Cermak102431d2018-01-29 10:36:07 +0000609 private void resetButtonsLayoutParams() {
610 final int childCount = getChildCount();
611 for (int i = 0; i < childCount; i++) {
612 final View child = getChildAt(i);
613 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
614 lp.show = false;
615 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_NONE;
616 }
617 }
618
619 private int squeezeButton(Button button, int heightMeasureSpec) {
620 final int estimatedOptimalTextWidth = estimateOptimalSqueezedButtonTextWidth(button);
621 if (estimatedOptimalTextWidth == SQUEEZE_FAILED) {
622 return SQUEEZE_FAILED;
623 }
624 return squeezeButtonToTextWidth(button, heightMeasureSpec, estimatedOptimalTextWidth);
625 }
626
627 private int estimateOptimalSqueezedButtonTextWidth(Button button) {
628 // Find a line-break point in the middle of the smart reply button text.
629 final String rawText = button.getText().toString();
630
631 // The button sometimes has a transformation affecting text layout (e.g. all caps).
632 final TransformationMethod transformation = button.getTransformationMethod();
633 final String text = transformation == null ?
634 rawText : transformation.getTransformation(rawText, button).toString();
635 final int length = text.length();
636 mBreakIterator.setText(text);
637
638 if (mBreakIterator.preceding(length / 2) == BreakIterator.DONE) {
639 if (mBreakIterator.next() == BreakIterator.DONE) {
640 // Can't find a single possible line break in either direction.
641 return SQUEEZE_FAILED;
642 }
643 }
644
645 final TextPaint paint = button.getPaint();
646 final int initialPosition = mBreakIterator.current();
647 final float initialLeftTextWidth = Layout.getDesiredWidth(text, 0, initialPosition, paint);
648 final float initialRightTextWidth =
649 Layout.getDesiredWidth(text, initialPosition, length, paint);
650 float optimalTextWidth = Math.max(initialLeftTextWidth, initialRightTextWidth);
651
652 if (initialLeftTextWidth != initialRightTextWidth) {
653 // See if there's a better line-break point (leading to a more narrow button) in
654 // either left or right direction.
655 final boolean moveLeft = initialLeftTextWidth > initialRightTextWidth;
656 final int maxSqueezeRemeasureAttempts = mConstants.getMaxSqueezeRemeasureAttempts();
657 for (int i = 0; i < maxSqueezeRemeasureAttempts; i++) {
658 final int newPosition =
659 moveLeft ? mBreakIterator.previous() : mBreakIterator.next();
660 if (newPosition == BreakIterator.DONE) {
661 break;
662 }
663
664 final float newLeftTextWidth = Layout.getDesiredWidth(text, 0, newPosition, paint);
665 final float newRightTextWidth =
666 Layout.getDesiredWidth(text, newPosition, length, paint);
667 final float newOptimalTextWidth = Math.max(newLeftTextWidth, newRightTextWidth);
668 if (newOptimalTextWidth < optimalTextWidth) {
669 optimalTextWidth = newOptimalTextWidth;
670 } else {
671 break;
672 }
673
674 boolean tooFar = moveLeft
675 ? newLeftTextWidth <= newRightTextWidth
676 : newLeftTextWidth >= newRightTextWidth;
677 if (tooFar) {
678 break;
679 }
680 }
681 }
682
683 return (int) Math.ceil(optimalTextWidth);
684 }
685
Gustav Senntoneab53682018-11-01 16:30:23 +0000686 /**
687 * Returns the combined width of the left drawable (the action icon) and the padding between the
688 * drawable and the button text.
689 */
690 private int getLeftCompoundDrawableWidthWithPadding(Button button) {
691 Drawable[] drawables = button.getCompoundDrawables();
692 Drawable leftDrawable = drawables[0];
693 if (leftDrawable == null) return 0;
694
695 return leftDrawable.getBounds().width() + button.getCompoundDrawablePadding();
696 }
697
Petr Cermak102431d2018-01-29 10:36:07 +0000698 private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
699 int oldWidth = button.getMeasuredWidth();
700 if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
701 // Correct for the fact that the button was laid out with single-line horizontal
702 // padding.
703 oldWidth += mSingleToDoubleLineButtonWidthIncrease;
704 }
705
706 // Re-measure the squeezed smart reply button.
707 button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
708 mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
709 final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
Gustav Senntoneab53682018-11-01 16:30:23 +0000710 2 * mDoubleLineButtonPaddingHorizontal + textWidth
711 + getLeftCompoundDrawableWidthWithPadding(button), MeasureSpec.AT_MOST);
Petr Cermak102431d2018-01-29 10:36:07 +0000712 button.measure(widthMeasureSpec, heightMeasureSpec);
713
714 final int newWidth = button.getMeasuredWidth();
715
716 final LayoutParams lp = (LayoutParams) button.getLayoutParams();
717 if (button.getLineCount() > 2 || newWidth >= oldWidth) {
718 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_FAILED;
719 return SQUEEZE_FAILED;
720 } else {
721 lp.squeezeStatus = LayoutParams.SQUEEZE_STATUS_PENDING;
722 return oldWidth - newWidth;
723 }
724 }
725
Milo Sredkova5bacea2018-04-12 12:52:43 +0100726 private void remeasureButtonsIfNecessary(
Petr Cermak102431d2018-01-29 10:36:07 +0000727 int buttonPaddingHorizontal, int maxChildHeight) {
Petr Cermak102431d2018-01-29 10:36:07 +0000728 final int maxChildHeightMeasure =
729 MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY);
730
731 final int childCount = getChildCount();
732 for (int i = 0; i < childCount; i++) {
733 final View child = getChildAt(i);
734 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
735 if (!lp.show) {
736 continue;
737 }
738
Petr Cermak102431d2018-01-29 10:36:07 +0000739 boolean requiresNewMeasure = false;
740 int newWidth = child.getMeasuredWidth();
741
742 // Re-measure reason 1: The button needs to be un-squeezed (either because it resulted
743 // in more than two lines or because it was unnecessary).
744 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_FAILED) {
745 requiresNewMeasure = true;
746 newWidth = Integer.MAX_VALUE;
747 }
748
749 // Re-measure reason 2: The button's horizontal padding is incorrect (because it was
750 // measured with the wrong number of lines).
751 if (child.getPaddingLeft() != buttonPaddingHorizontal) {
752 requiresNewMeasure = true;
Kenny Guy48ee6d62018-05-09 16:51:26 +0100753 if (newWidth != Integer.MAX_VALUE) {
754 if (buttonPaddingHorizontal == mSingleLineButtonPaddingHorizontal) {
755 // Change padding (2->1 line).
756 newWidth -= mSingleToDoubleLineButtonWidthIncrease;
757 } else {
758 // Change padding (1->2 lines).
759 newWidth += mSingleToDoubleLineButtonWidthIncrease;
760 }
Petr Cermak102431d2018-01-29 10:36:07 +0000761 }
762 child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
763 buttonPaddingHorizontal, child.getPaddingBottom());
764 }
765
766 // Re-measure reason 3: The button's height is less than the max height of all buttons
767 // (all should have the same height).
768 if (child.getMeasuredHeight() != maxChildHeight) {
769 requiresNewMeasure = true;
770 }
771
772 if (requiresNewMeasure) {
773 child.measure(MeasureSpec.makeMeasureSpec(newWidth, MeasureSpec.AT_MOST),
774 maxChildHeightMeasure);
775 }
776 }
777 }
778
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000779 private void markButtonsWithPendingSqueezeStatusAs(
780 int squeezeStatus, List<View> coveredChildren) {
781 for (View child : coveredChildren) {
Petr Cermak102431d2018-01-29 10:36:07 +0000782 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
783 if (lp.squeezeStatus == LayoutParams.SQUEEZE_STATUS_PENDING) {
784 lp.squeezeStatus = squeezeStatus;
785 }
786 }
787 }
788
789 @Override
790 protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
791 final boolean isRtl = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
792
793 final int width = right - left;
794 int position = isRtl ? width - mPaddingRight : mPaddingLeft;
795
796 final int childCount = getChildCount();
797 for (int i = 0; i < childCount; i++) {
798 final View child = getChildAt(i);
799 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
800 if (!lp.show) {
801 continue;
802 }
803
804 final int childWidth = child.getMeasuredWidth();
805 final int childHeight = child.getMeasuredHeight();
806 final int childLeft = isRtl ? position - childWidth : position;
807 child.layout(childLeft, 0, childLeft + childWidth, childHeight);
808
809 final int childWidthWithSpacing = childWidth + mSpacing;
810 if (isRtl) {
811 position -= childWidthWithSpacing;
812 } else {
813 position += childWidthWithSpacing;
814 }
815 }
816 }
817
818 @Override
819 protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
820 final LayoutParams lp = (LayoutParams) child.getLayoutParams();
821 return lp.show && super.drawChild(canvas, child, drawingTime);
822 }
823
Kenny Guy14d035c2018-05-02 19:10:36 +0100824 public void setBackgroundTintColor(int backgroundColor) {
825 if (backgroundColor == mCurrentBackgroundColor) {
826 // Same color ignoring.
827 return;
828 }
829 mCurrentBackgroundColor = backgroundColor;
830
Lucas Dupina291d192018-06-07 13:59:42 -0700831 final boolean dark = !ContrastColorUtil.isColorLight(backgroundColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100832
Lucas Dupina291d192018-06-07 13:59:42 -0700833 int textColor = ContrastColorUtil.ensureTextContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100834 dark ? mDefaultTextColorDarkBg : mDefaultTextColor,
835 backgroundColor | 0xff000000, dark);
Lucas Dupina291d192018-06-07 13:59:42 -0700836 int strokeColor = ContrastColorUtil.ensureContrast(
Kenny Guy14d035c2018-05-02 19:10:36 +0100837 mDefaultStrokeColor, backgroundColor | 0xff000000, dark, mMinStrokeContrast);
838 int rippleColor = dark ? mRippleColorDarkBg : mRippleColor;
839
840 int childCount = getChildCount();
841 for (int i = 0; i < childCount; i++) {
842 final Button child = (Button) getChildAt(i);
Gustav Sennton5759f872019-02-13 17:25:26 +0000843 setButtonColors(child, backgroundColor, strokeColor, textColor, rippleColor,
844 mStrokeWidth);
Kenny Guy14d035c2018-05-02 19:10:36 +0100845 }
846 }
847
Gustav Sennton5759f872019-02-13 17:25:26 +0000848 private static void setButtonColors(Button button, int backgroundColor, int strokeColor,
849 int textColor, int rippleColor, int strokeWidth) {
Kenny Guy14d035c2018-05-02 19:10:36 +0100850 Drawable drawable = button.getBackground();
851 if (drawable instanceof RippleDrawable) {
852 // Mutate in case other notifications are using this drawable.
853 drawable = drawable.mutate();
854 RippleDrawable ripple = (RippleDrawable) drawable;
855 ripple.setColor(ColorStateList.valueOf(rippleColor));
856 Drawable inset = ripple.getDrawable(0);
857 if (inset instanceof InsetDrawable) {
858 Drawable background = ((InsetDrawable) inset).getDrawable();
859 if (background instanceof GradientDrawable) {
860 GradientDrawable gradientDrawable = (GradientDrawable) background;
861 gradientDrawable.setColor(backgroundColor);
Gustav Sennton5759f872019-02-13 17:25:26 +0000862 gradientDrawable.setStroke(strokeWidth, strokeColor);
Kenny Guy14d035c2018-05-02 19:10:36 +0100863 }
864 }
865 button.setBackground(drawable);
866 }
867 button.setTextColor(textColor);
868 }
869
Gustav Sennton3c5fcca2019-01-16 17:05:03 +0000870 private void setCornerRadius(Button button, float radius) {
871 Drawable drawable = button.getBackground();
872 if (drawable instanceof RippleDrawable) {
873 // Mutate in case other notifications are using this drawable.
874 drawable = drawable.mutate();
875 RippleDrawable ripple = (RippleDrawable) drawable;
876 Drawable inset = ripple.getDrawable(0);
877 if (inset instanceof InsetDrawable) {
878 Drawable background = ((InsetDrawable) inset).getDrawable();
879 if (background instanceof GradientDrawable) {
880 GradientDrawable gradientDrawable = (GradientDrawable) background;
881 gradientDrawable.setCornerRadius(radius);
882 }
883 }
884 }
885 }
886
Gustav Senntoneab53682018-11-01 16:30:23 +0000887 private ActivityStarter getActivityStarter() {
888 if (mActivityStarter == null) {
889 mActivityStarter = Dependency.get(ActivityStarter.class);
890 }
891 return mActivityStarter;
892 }
893
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000894 private enum SmartButtonType {
895 REPLY,
896 ACTION
897 }
898
Petr Cermak102431d2018-01-29 10:36:07 +0000899 @VisibleForTesting
900 static class LayoutParams extends ViewGroup.LayoutParams {
901
902 /** Button is not squeezed. */
903 private static final int SQUEEZE_STATUS_NONE = 0;
904
905 /**
906 * Button was successfully squeezed, but it might be un-squeezed later if the squeezing
907 * turns out to have been unnecessary (because there's still not enough space to add another
908 * button).
909 */
910 private static final int SQUEEZE_STATUS_PENDING = 1;
911
912 /** Button was successfully squeezed and it won't be un-squeezed. */
913 private static final int SQUEEZE_STATUS_SUCCESSFUL = 2;
914
915 /**
916 * Button wasn't successfully squeezed. The squeezing resulted in more than two lines of
917 * text or it didn't reduce the button's width at all. The button will have to be
918 * re-measured to use only one line of text.
919 */
920 private static final int SQUEEZE_STATUS_FAILED = 3;
921
922 private boolean show = false;
923 private int squeezeStatus = SQUEEZE_STATUS_NONE;
Gustav Senntonb149a1a2018-11-20 17:25:50 +0000924 private SmartButtonType buttonType = SmartButtonType.REPLY;
Petr Cermak102431d2018-01-29 10:36:07 +0000925
926 private LayoutParams(Context c, AttributeSet attrs) {
927 super(c, attrs);
928 }
929
930 private LayoutParams(int width, int height) {
931 super(width, height);
932 }
933
934 @VisibleForTesting
935 boolean isShown() {
936 return show;
937 }
938 }
Tony Mak29996702018-11-26 16:23:34 +0000939
940 /**
941 * Data class for smart replies.
942 */
943 public static class SmartReplies {
944 @NonNull
945 public final RemoteInput remoteInput;
946 @NonNull
947 public final PendingIntent pendingIntent;
948 @NonNull
949 public final CharSequence[] choices;
950 public final boolean fromAssistant;
951
952 public SmartReplies(CharSequence[] choices, RemoteInput remoteInput,
953 PendingIntent pendingIntent, boolean fromAssistant) {
954 this.choices = choices;
955 this.remoteInput = remoteInput;
956 this.pendingIntent = pendingIntent;
957 this.fromAssistant = fromAssistant;
958 }
959 }
960
961
962 /**
963 * Data class for smart actions.
964 */
965 public static class SmartActions {
966 @NonNull
967 public final List<Notification.Action> actions;
968 public final boolean fromAssistant;
969
970 public SmartActions(List<Notification.Action> actions, boolean fromAssistant) {
971 this.actions = actions;
972 this.fromAssistant = fromAssistant;
973 }
974 }
Gustav Sennton8a52dc32019-04-15 12:48:23 +0100975
976 /**
977 * An OnClickListener wrapper that blocks the underlying OnClickListener for a given amount of
978 * time.
979 */
980 private static class DelayedOnClickListener implements OnClickListener {
981 private final OnClickListener mActualListener;
982 private final long mInitDelayMs;
983 private final long mInitTimeMs;
984
985 DelayedOnClickListener(OnClickListener actualOnClickListener, long initDelayMs) {
986 mActualListener = actualOnClickListener;
987 mInitDelayMs = initDelayMs;
988 mInitTimeMs = SystemClock.elapsedRealtime();
989 }
990
991 public void onClick(View v) {
992 if (hasFinishedInitialization()) {
993 mActualListener.onClick(v);
994 } else {
995 Log.i(TAG, "Accidental Smart Suggestion click registered, delay: " + mInitDelayMs);
996 }
997 }
998
999 private boolean hasFinishedInitialization() {
1000 return SystemClock.elapsedRealtime() >= mInitTimeMs + mInitDelayMs;
1001 }
1002 }
Petr Cermaked7429c2017-12-18 19:38:04 +00001003}