Merge "Add smart actions to message notifications."
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 0124a68..f2a3e44 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -3193,6 +3193,25 @@
}
/**
+ * Returns the actions that are contextual (marked as SEMANTIC_ACTION_CONTEXTUAL_SUGGESTION) out
+ * of the actions in this notification.
+ *
+ * @hide
+ */
+ public List<Notification.Action> getContextualActions() {
+ if (actions == null) return Collections.emptyList();
+
+ List<Notification.Action> contextualActions = new ArrayList<>();
+ for (Notification.Action action : actions) {
+ if (action.getSemanticAction()
+ == Notification.Action.SEMANTIC_ACTION_CONTEXTUAL_SUGGESTION) {
+ contextualActions.add(action);
+ }
+ }
+ return contextualActions;
+ }
+
+ /**
* Builder class for {@link Notification} objects.
*
* Provides a convenient way to set the various fields of a {@link Notification} and generate
diff --git a/packages/SystemUI/res/layout/smart_action_button.xml b/packages/SystemUI/res/layout/smart_action_button.xml
new file mode 100644
index 0000000..2716034
--- /dev/null
+++ b/packages/SystemUI/res/layout/smart_action_button.xml
@@ -0,0 +1,34 @@
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<!-- android:paddingHorizontal is set dynamically in SmartReplyView. -->
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ style="@android:style/Widget.Material.Button"
+ android:stateListAnimator="@null"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:minWidth="0dp"
+ android:minHeight="@dimen/smart_reply_button_min_height"
+ android:paddingVertical="@dimen/smart_reply_button_padding_vertical"
+ android:background="@drawable/smart_reply_button_background"
+ android:gravity="center"
+ android:fontFamily="roboto-medium"
+ android:textSize="@dimen/smart_reply_button_font_size"
+ android:lineSpacingExtra="@dimen/smart_reply_button_line_spacing_extra"
+ android:textColor="@color/smart_reply_button_text"
+ android:drawablePadding="@dimen/smart_action_button_icon_padding"
+ android:textStyle="normal"
+ android:ellipsize="none"/>
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 07628c6..0997c5b1 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -881,6 +881,7 @@
<dimen name="smart_reply_button_stroke_width">1dp</dimen>
<dimen name="smart_reply_button_font_size">14sp</dimen>
<dimen name="smart_reply_button_line_spacing_extra">6sp</dimen> <!-- Total line height 20sp. -->
+ <dimen name="smart_action_button_icon_padding">10dp</dimen>
<!-- A reasonable upper bound for the height of the smart reply button. The measuring code
needs to start with a guess for the maximum size. Currently two-line smart reply buttons
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
index fa3fa5b..bb9a341 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java
@@ -55,6 +55,8 @@
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.statusbar.policy.SmartReplyView;
+import java.util.List;
+
/**
* A frame layout containing the actual payload of the notification, including the contracted,
* expanded and heads up layout. This class is responsible for clipping the content and and
@@ -1285,38 +1287,88 @@
return;
}
- Notification notification = entry.notification.getNotification();
+ SmartRepliesAndActions smartRepliesAndActions = chooseSmartRepliesAndActions(
+ mSmartReplyConstants, entry);
- Pair<RemoteInput, Notification.Action> remoteInputActionPair =
- entry.notification.getNotification().findRemoteInputActionPair(false /*freeform */);
- Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair =
- notification.findRemoteInputActionPair(true /*freeform */);
+ applyRemoteInput(entry, smartRepliesAndActions.freeformRemoteInputActionPair != null);
+ applySmartReplyView(smartRepliesAndActions, entry);
+ }
- boolean enableAppGeneratedSmartReplies = (mSmartReplyConstants.isEnabled()
- && (!mSmartReplyConstants.requiresTargetingP()
+ /**
+ * Chose what smart replies and smart actions to display. App generated suggestions take
+ * precedence. So if the app provides any smart replies, we don't show any
+ * replies or actions generated by the NotificationAssistantService (NAS), and if the app
+ * provides any smart actions we also don't show any NAS-generated replies or actions.
+ */
+ @VisibleForTesting
+ static SmartRepliesAndActions chooseSmartRepliesAndActions(
+ SmartReplyConstants smartReplyConstants,
+ final NotificationData.Entry entry) {
+ boolean enableAppGeneratedSmartReplies = (smartReplyConstants.isEnabled()
+ && (!smartReplyConstants.requiresTargetingP()
|| entry.targetSdk >= Build.VERSION_CODES.P));
- RemoteInput remoteInputWithChoices = null;
- PendingIntent pendingIntentWithChoices= null;
- CharSequence[] choices = null;
- if (enableAppGeneratedSmartReplies
- && remoteInputActionPair != null
- && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())) {
- // app generated smart replies
- remoteInputWithChoices = remoteInputActionPair.first;
- pendingIntentWithChoices = remoteInputActionPair.second.actionIntent;
- choices = remoteInputActionPair.first.getChoices();
+ Notification notification = entry.notification.getNotification();
+ Pair<RemoteInput, Notification.Action> remoteInputActionPair =
+ notification.findRemoteInputActionPair(false /* freeform */);
+ Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair =
+ notification.findRemoteInputActionPair(true /* freeform */);
+
+ boolean appGeneratedSmartRepliesExist =
+ enableAppGeneratedSmartReplies
+ && remoteInputActionPair != null
+ && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices());
+
+ List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions();
+ boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty();
+
+ if (appGeneratedSmartRepliesExist) {
+ return new SmartRepliesAndActions(remoteInputActionPair.first,
+ remoteInputActionPair.second.actionIntent,
+ remoteInputActionPair.first.getChoices(),
+ appGeneratedSmartActions,
+ freeformRemoteInputActionPair);
+ } else if (appGeneratedSmartActionsExist) {
+ return new SmartRepliesAndActions(null, null, null, appGeneratedSmartActions,
+ freeformRemoteInputActionPair);
} else if (!ArrayUtils.isEmpty(entry.smartReplies)
&& freeformRemoteInputActionPair != null
&& freeformRemoteInputActionPair.second.getAllowGeneratedReplies()) {
- // system generated smart replies
- remoteInputWithChoices = freeformRemoteInputActionPair.first;
- pendingIntentWithChoices = freeformRemoteInputActionPair.second.actionIntent;
- choices = entry.smartReplies;
+ // App didn't generate anything, use NAS-generated replies and actions
+ return new SmartRepliesAndActions(freeformRemoteInputActionPair.first,
+ freeformRemoteInputActionPair.second.actionIntent,
+ entry.smartReplies,
+ entry.systemGeneratedSmartActions,
+ freeformRemoteInputActionPair);
+ }
+ // App didn't generate anything, and there are no NAS-generated smart replies.
+ return new SmartRepliesAndActions(null, null, null, entry.systemGeneratedSmartActions,
+ freeformRemoteInputActionPair);
+ }
+
+ @VisibleForTesting
+ static class SmartRepliesAndActions {
+ public final RemoteInput remoteInputWithChoices;
+ public final PendingIntent pendingIntentForSmartReplies;
+ public final CharSequence[] smartReplies;
+ public final List<Notification.Action> smartActions;
+ public final Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair;
+
+ SmartRepliesAndActions(RemoteInput remoteInput, PendingIntent pendingIntent,
+ CharSequence[] choices, List<Notification.Action> smartActions,
+ Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair) {
+ this.remoteInputWithChoices = remoteInput;
+ this.pendingIntentForSmartReplies = pendingIntent;
+ this.smartReplies = choices;
+ this.smartActions = smartActions;
+ this.freeformRemoteInputActionPair = freeformRemoteInputActionPair;
}
- applyRemoteInput(entry, freeformRemoteInputActionPair != null);
- applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices, entry, choices);
+ boolean smartRepliesExist() {
+ return remoteInputWithChoices != null
+ && pendingIntentForSmartReplies != null
+ && !ArrayUtils.isEmpty(smartReplies);
+ }
}
private void applyRemoteInput(NotificationData.Entry entry, boolean hasFreeformRemoteInput) {
@@ -1418,28 +1470,32 @@
return null;
}
- private void applySmartReplyView(RemoteInput remoteInput, PendingIntent pendingIntent,
- NotificationData.Entry entry, CharSequence[] choices) {
+ private void applySmartReplyView(SmartRepliesAndActions smartRepliesAndActions,
+ NotificationData.Entry entry) {
if (mExpandedChild != null) {
mExpandedSmartReplyView =
- applySmartReplyView(mExpandedChild, remoteInput, pendingIntent, entry, choices);
- if (mExpandedSmartReplyView != null && remoteInput != null
- && choices != null && choices.length > 0) {
- mSmartReplyController.smartRepliesAdded(entry, choices.length);
+ applySmartReplyView(mExpandedChild, smartRepliesAndActions, entry);
+ if (mExpandedSmartReplyView != null
+ && smartRepliesAndActions.remoteInputWithChoices != null
+ && smartRepliesAndActions.smartReplies != null
+ && smartRepliesAndActions.smartReplies.length > 0) {
+ mSmartReplyController.smartRepliesAdded(entry,
+ smartRepliesAndActions.smartReplies.length);
}
}
}
- private SmartReplyView applySmartReplyView(
- View view, RemoteInput remoteInput, PendingIntent pendingIntent,
- NotificationData.Entry entry, CharSequence[] choices) {
+ private SmartReplyView applySmartReplyView(View view,
+ SmartRepliesAndActions smartRepliesAndActions, NotificationData.Entry entry) {
View smartReplyContainerCandidate = view.findViewById(
com.android.internal.R.id.smart_reply_container);
if (!(smartReplyContainerCandidate instanceof LinearLayout)) {
return null;
}
LinearLayout smartReplyContainer = (LinearLayout) smartReplyContainerCandidate;
- if (remoteInput == null || pendingIntent == null) {
+ // If there are no smart replies and no smart actions - early out.
+ if (!smartRepliesAndActions.smartRepliesExist()
+ && smartRepliesAndActions.smartActions.isEmpty()) {
smartReplyContainer.setVisibility(View.GONE);
return null;
}
@@ -1468,9 +1524,11 @@
}
}
if (smartReplyView != null) {
- smartReplyView.setRepliesFromRemoteInput(remoteInput, pendingIntent,
- mSmartReplyController, entry, smartReplyContainer, choices
- );
+ smartReplyView.resetSmartSuggestions(smartReplyContainer);
+ smartReplyView.addRepliesFromRemoteInput(smartRepliesAndActions.remoteInputWithChoices,
+ smartRepliesAndActions.pendingIntentForSmartReplies, mSmartReplyController,
+ entry, smartRepliesAndActions.smartReplies);
+ smartReplyView.addSmartActions(smartRepliesAndActions.smartActions);
smartReplyContainer.setVisibility(View.VISIBLE);
}
return smartReplyView;
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
index 42f1378..0186683 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java
@@ -1,6 +1,7 @@
package com.android.systemui.statusbar.policy;
import android.annotation.ColorInt;
+import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
@@ -19,6 +20,7 @@
import android.text.method.TransformationMethod;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.Size;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -30,6 +32,7 @@
import com.android.internal.util.ContrastColorUtil;
import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.statusbar.SmartReplyController;
import com.android.systemui.statusbar.notification.NotificationData;
@@ -38,14 +41,15 @@
import java.text.BreakIterator;
import java.util.Comparator;
+import java.util.List;
import java.util.PriorityQueue;
-/** View which displays smart reply buttons in notifications. */
+/** View which displays smart reply and smart actions buttons in notifications. */
public class SmartReplyView extends ViewGroup {
private static final String TAG = "SmartReplyView";
- private static final int MEASURE_SPEC_ANY_WIDTH =
+ private static final int MEASURE_SPEC_ANY_LENGTH =
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
private static final Comparator<View> DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR =
@@ -98,6 +102,8 @@
private final int mStrokeWidth;
private final double mMinStrokeContrast;
+ private ActivityStarter mActivityStarter;
+
public SmartReplyView(Context context, AttributeSet attrs) {
super(context, attrs);
mConstants = Dependency.get(SmartReplyConstants.class);
@@ -168,13 +174,24 @@
Math.max(getChildCount(), 1), DECREASING_MEASURED_WIDTH_WITHOUT_PADDING_COMPARATOR);
}
- public void setRepliesFromRemoteInput(
- RemoteInput remoteInput, PendingIntent pendingIntent,
- SmartReplyController smartReplyController, NotificationData.Entry entry,
- View smartReplyContainer, CharSequence[] choices) {
- mSmartReplyContainer = smartReplyContainer;
+ /**
+ * Reset the smart suggestions view to allow adding new replies and actions.
+ */
+ public void resetSmartSuggestions(View newSmartReplyContainer) {
+ mSmartReplyContainer = newSmartReplyContainer;
removeAllViews();
mCurrentBackgroundColor = mDefaultBackgroundColor;
+ }
+
+ /**
+ * Add smart replies to this view, using the provided {@link RemoteInput} and
+ * {@link PendingIntent} to respond when the user taps a smart reply. Only the replies that fit
+ * into the notification are shown.
+ */
+ public void addRepliesFromRemoteInput(
+ RemoteInput remoteInput, PendingIntent pendingIntent,
+ SmartReplyController smartReplyController, NotificationData.Entry entry,
+ CharSequence[] choices) {
if (remoteInput != null && pendingIntent != null) {
if (choices != null) {
for (int i = 0; i < choices.length; ++i) {
@@ -188,6 +205,22 @@
reallocateCandidateButtonQueueForSqueezing();
}
+ /**
+ * Add smart actions to be shown next to smart replies. Only the actions that fit into the
+ * notification are shown.
+ */
+ public void addSmartActions(List<Notification.Action> smartActions) {
+ int numSmartActions = smartActions.size();
+ for (int n = 0; n < numSmartActions; n++) {
+ Notification.Action action = smartActions.get(n);
+ if (action.actionIntent != null) {
+ Button actionButton = inflateActionButton(getContext(), this, action);
+ addView(actionButton);
+ }
+ }
+ reallocateCandidateButtonQueueForSqueezing();
+ }
+
public static SmartReplyView inflate(Context context, ViewGroup root) {
return (SmartReplyView)
LayoutInflater.from(context).inflate(R.layout.smart_reply_view, root, false);
@@ -234,6 +267,48 @@
return b;
}
+ @VisibleForTesting
+ Button inflateActionButton(Context context, ViewGroup root, Notification.Action action) {
+ Button button = (Button) LayoutInflater.from(context).inflate(
+ R.layout.smart_action_button, root, false);
+ button.setText(action.title);
+
+ Drawable iconDrawable = action.getIcon().loadDrawable(context);
+ // Add the action icon to the Smart Action button.
+ Size newIconSize = calculateIconSizeFromSingleLineButton(context, root,
+ new Size(iconDrawable.getIntrinsicWidth(), iconDrawable.getIntrinsicHeight()));
+ iconDrawable.setBounds(0, 0, newIconSize.getWidth(), newIconSize.getHeight());
+ button.setCompoundDrawables(iconDrawable, null, null, null);
+
+ button.setOnClickListener(view ->
+ getActivityStarter().startPendingIntentDismissingKeyguard(action.actionIntent));
+
+ // TODO(b/119010281): handle accessibility
+
+ return button;
+ }
+
+ private static Size calculateIconSizeFromSingleLineButton(Context context, ViewGroup root,
+ Size originalIconSize) {
+ Button button = (Button) LayoutInflater.from(context).inflate(
+ R.layout.smart_action_button, root, false);
+ // Add simple text here to ensure the button displays one line of text.
+ button.setText("a");
+ return calculateIconSizeFromButtonHeight(button, originalIconSize);
+ }
+
+ // Given a button with text on a single line - we want to add an icon to that button. This
+ // method calculates the icon height to use to avoid making the button grow in height.
+ private static Size calculateIconSizeFromButtonHeight(Button button, Size originalIconSize) {
+ // A completely permissive measure spec should make the button text single-line.
+ button.measure(MEASURE_SPEC_ANY_LENGTH, MEASURE_SPEC_ANY_LENGTH);
+ int buttonHeight = button.getMeasuredHeight();
+ int newIconHeight = buttonHeight / 2;
+ int newIconWidth = (int) (originalIconSize.getWidth()
+ * ((double) newIconHeight) / originalIconSize.getHeight());
+ return new Size(newIconWidth, newIconHeight);
+ }
+
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(mContext, attrs);
@@ -277,7 +352,7 @@
child.setPadding(buttonPaddingHorizontal, child.getPaddingTop(),
buttonPaddingHorizontal, child.getPaddingBottom());
- child.measure(MEASURE_SPEC_ANY_WIDTH, heightMeasureSpec);
+ child.measure(MEASURE_SPEC_ANY_LENGTH, heightMeasureSpec);
final int lineCount = ((Button) child).getLineCount();
if (lineCount < 1 || lineCount > 2) {
@@ -437,6 +512,18 @@
return (int) Math.ceil(optimalTextWidth);
}
+ /**
+ * Returns the combined width of the left drawable (the action icon) and the padding between the
+ * drawable and the button text.
+ */
+ private int getLeftCompoundDrawableWidthWithPadding(Button button) {
+ Drawable[] drawables = button.getCompoundDrawables();
+ Drawable leftDrawable = drawables[0];
+ if (leftDrawable == null) return 0;
+
+ return leftDrawable.getBounds().width() + button.getCompoundDrawablePadding();
+ }
+
private int squeezeButtonToTextWidth(Button button, int heightMeasureSpec, int textWidth) {
int oldWidth = button.getMeasuredWidth();
if (button.getPaddingLeft() != mDoubleLineButtonPaddingHorizontal) {
@@ -449,7 +536,8 @@
button.setPadding(mDoubleLineButtonPaddingHorizontal, button.getPaddingTop(),
mDoubleLineButtonPaddingHorizontal, button.getPaddingBottom());
final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(
- 2 * mDoubleLineButtonPaddingHorizontal + textWidth, MeasureSpec.AT_MOST);
+ 2 * mDoubleLineButtonPaddingHorizontal + textWidth
+ + getLeftCompoundDrawableWidthWithPadding(button), MeasureSpec.AT_MOST);
button.measure(widthMeasureSpec, heightMeasureSpec);
final int newWidth = button.getMeasuredWidth();
@@ -607,6 +695,13 @@
button.setTextColor(textColor);
}
+ private ActivityStarter getActivityStarter() {
+ if (mActivityStarter == null) {
+ mActivityStarter = Dependency.get(ActivityStarter.class);
+ }
+ return mActivityStarter;
+ }
+
@VisibleForTesting
static class LayoutParams extends ViewGroup.LayoutParams {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
index c189c95..a6725b8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentViewTest.java
@@ -16,6 +16,11 @@
package com.android.systemui.statusbar.notification.row;
+import static org.hamcrest.Matchers.empty;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.Mockito.doNothing;
@@ -28,29 +33,62 @@
import static org.mockito.Mockito.when;
import android.app.AppOpsManager;
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.service.notification.StatusBarNotification;
import android.support.test.annotation.UiThreadTest;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.util.ArraySet;
+import android.util.Pair;
import android.view.NotificationHeaderView;
import android.view.View;
+import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.NotificationData;
+import com.android.systemui.statusbar.policy.SmartReplyConstants;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.ArrayList;
+import java.util.List;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class NotificationContentViewTest extends SysuiTestCase {
+ private static final String TEST_ACTION = "com.android.SMART_REPLY_VIEW_ACTION";
+
NotificationContentView mView;
+ @Mock
+ SmartReplyConstants mSmartReplyConstants;
+ @Mock
+ StatusBarNotification mStatusBarNotification;
+ @Mock
+ Notification mNotification;
+ NotificationData.Entry mEntry;
+ @Mock
+ RemoteInput mRemoteInput;
+ @Mock
+ RemoteInput mFreeFormRemoteInput;
+
+ private Icon mActionIcon;
+
+
@Before
@UiThreadTest
public void setup() {
+ MockitoAnnotations.initMocks(this);
+
mView = new NotificationContentView(mContext, null);
ExpandableNotificationRow row = new ExpandableNotificationRow(mContext, null);
ExpandableNotificationRow mockRow = spy(row);
@@ -67,6 +105,12 @@
mView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight());
+
+ // Smart replies
+ when(mStatusBarNotification.getNotification()).thenReturn(mNotification);
+ mEntry = new NotificationData.Entry(mStatusBarNotification);
+ when(mSmartReplyConstants.isEnabled()).thenReturn(true);
+ mActionIcon = Icon.createWithResource(mContext, R.drawable.ic_person);
}
private View createViewWithHeight(int height) {
@@ -82,7 +126,7 @@
mView.setDark(true, false, 0);
mView.setDark(false, true, 0);
mView.setHeadsUpAnimatingAway(true);
- Assert.assertFalse(mView.isAnimatingVisibleType());
+ assertFalse(mView.isAnimatingVisibleType());
}
@Test
@@ -115,4 +159,161 @@
verify(mockAmbient, never()).showAppOpsIcons(ops);
verify(mockHeadsUp, times(1)).showAppOpsIcons(any());
}
+
+ private void setupAppGeneratedReplies(CharSequence[] smartReplyTitles) {
+ Notification.Action freeFormAction =
+ new Notification.Action.Builder(null, "Freeform Test Action", null).build();
+ setupAppGeneratedReplies(smartReplyTitles, freeFormAction);
+ }
+
+ private void setupAppGeneratedReplies(
+ CharSequence[] smartReplyTitles,
+ Notification.Action freeFormRemoteInputAction) {
+ Notification.Action action =
+ new Notification.Action.Builder(null, "Test Action", null).build();
+ when(mRemoteInput.getChoices()).thenReturn(smartReplyTitles);
+ Pair<RemoteInput, Notification.Action> remoteInputActionPair =
+ Pair.create(mRemoteInput, action);
+ when(mNotification.findRemoteInputActionPair(false)).thenReturn(remoteInputActionPair);
+
+ Pair<RemoteInput, Notification.Action> freeFormRemoteInputActionPair =
+ Pair.create(mFreeFormRemoteInput, freeFormRemoteInputAction);
+ when(mNotification.findRemoteInputActionPair(true)).thenReturn(
+ freeFormRemoteInputActionPair);
+ }
+
+ @Test
+ public void chooseSmartRepliesAndActions_smartRepliesOff_noAppGeneratedSmartReplies() {
+ setupAppGeneratedReplies(new String[] {"Reply1", "Reply2"});
+ when(mSmartReplyConstants.isEnabled()).thenReturn(false);
+
+ NotificationContentView.SmartRepliesAndActions repliesAndActions =
+ NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+ assertFalse(repliesAndActions.smartRepliesExist());
+ }
+
+ @Test
+ public void chooseSmartRepliesAndActions_appGeneratedSmartReplies() {
+ CharSequence[] smartReplies = new String[] {"Reply1", "Reply2"};
+ setupAppGeneratedReplies(smartReplies);
+ when(mSmartReplyConstants.requiresTargetingP()).thenReturn(false);
+
+ NotificationContentView.SmartRepliesAndActions repliesAndActions =
+ NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+ assertThat(repliesAndActions.smartReplies, equalTo(smartReplies));
+ }
+
+ @Test
+ public void chooseSmartRepliesAndActions_appGeneratedSmartRepliesAndActions() {
+ CharSequence[] smartReplies = new String[] {"Reply1", "Reply2"};
+ setupAppGeneratedReplies(smartReplies);
+ when(mSmartReplyConstants.requiresTargetingP()).thenReturn(false);
+
+ List<Notification.Action> smartActions =
+ createActions(new String[] {"Test Action 1", "Test Action 2"});
+ when(mNotification.getContextualActions()).thenReturn(smartActions);
+
+ NotificationContentView.SmartRepliesAndActions repliesAndActions =
+ NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+ assertThat(repliesAndActions.smartReplies, equalTo(smartReplies));
+ assertThat(repliesAndActions.smartActions, equalTo(smartActions));
+ }
+
+ @Test
+ public void chooseSmartRepliesAndActions_sysGeneratedSmartReplies() {
+ Notification.Action freeFormAction = createActionBuilder("Freeform Action")
+ .setAllowGeneratedReplies(true)
+ .build();
+ // Pass a null-array as app-generated smart replies, so that we use NAS-generated smart
+ // replies.
+ setupAppGeneratedReplies(null, freeFormAction);
+
+ mEntry.smartReplies =
+ new String[] {"Sys Smart Reply 1", "Sys Smart Reply 2"};
+ NotificationContentView.SmartRepliesAndActions repliesAndActions =
+ NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+ assertThat(repliesAndActions.smartReplies, equalTo(mEntry.smartReplies));
+ assertThat(repliesAndActions.smartActions, is(empty()));
+ }
+
+ @Test
+ public void chooseSmartRepliesAndActions_noSysGeneratedSmartRepliesIfNotAllowed() {
+ Notification.Action freeFormAction = createActionBuilder("Freeform Action")
+ .setAllowGeneratedReplies(false)
+ .build();
+ // Pass a null-array as app-generated smart replies, so that we use NAS-generated smart
+ // replies.
+ setupAppGeneratedReplies(null, freeFormAction);
+
+ mEntry.smartReplies =
+ new String[] {"Sys Smart Reply 1", "Sys Smart Reply 2"};
+ NotificationContentView.SmartRepliesAndActions repliesAndActions =
+ NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+ assertThat(repliesAndActions.smartReplies, equalTo(null));
+ assertThat(repliesAndActions.smartActions, is(empty()));
+ }
+
+ @Test
+ public void chooseSmartRepliesAndActions_sysGeneratedSmartActions() {
+ // Pass a null-array as app-generated smart replies, so that we use NAS-generated smart
+ // actions.
+ setupAppGeneratedReplies(null);
+
+ mEntry.systemGeneratedSmartActions =
+ createActions(new String[] {"Sys Smart Action 1", "Sys Smart Action 2"});
+ NotificationContentView.SmartRepliesAndActions repliesAndActions =
+ NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+ assertThat(repliesAndActions.smartReplies, equalTo(null));
+ assertThat(repliesAndActions.smartActions, equalTo(mEntry.systemGeneratedSmartActions));
+ }
+
+ @Test
+ public void chooseSmartRepliesAndActions_appGenPreferredOverSysGen() {
+ Notification.Action freeFormAction = createActionBuilder("Freeform Action")
+ .setAllowGeneratedReplies(true)
+ .build();
+ CharSequence[] appGenSmartReplies = new String[] {"Reply1", "Reply2"};
+ // Pass a null-array as app-generated smart replies, so that we use NAS-generated smart
+ // replies.
+ setupAppGeneratedReplies(appGenSmartReplies, freeFormAction);
+ when(mSmartReplyConstants.requiresTargetingP()).thenReturn(false);
+
+ List<Notification.Action> appGenSmartActions =
+ createActions(new String[] {"Test Action 1", "Test Action 2"});
+ when(mNotification.getContextualActions()).thenReturn(appGenSmartActions);
+
+ mEntry.smartReplies = new String[] {"Sys Smart Reply 1", "Sys Smart Reply 2"};
+ mEntry.systemGeneratedSmartActions =
+ createActions(new String[] {"Sys Smart Action 1", "Sys Smart Action 2"});
+
+ NotificationContentView.SmartRepliesAndActions repliesAndActions =
+ NotificationContentView.chooseSmartRepliesAndActions(mSmartReplyConstants, mEntry);
+
+ assertThat(repliesAndActions.smartReplies, equalTo(appGenSmartReplies));
+ assertThat(repliesAndActions.smartActions, equalTo(appGenSmartActions));
+ }
+
+ private Notification.Action.Builder createActionBuilder(String actionTitle) {
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
+ new Intent(TEST_ACTION), 0);
+ return new Notification.Action.Builder(mActionIcon, actionTitle, pendingIntent);
+ }
+
+ private Notification.Action createAction(String actionTitle) {
+ return createActionBuilder(actionTitle).build();
+ }
+
+ private List<Notification.Action> createActions(String[] actionTitles) {
+ List<Notification.Action> actions = new ArrayList<>();
+ for (String title : actionTitles) {
+ actions.add(createAction(title));
+ }
+ return actions;
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
index 4534ebe..9e659c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/SmartReplyViewTest.java
@@ -22,7 +22,9 @@
import static junit.framework.Assert.assertTrue;
import static junit.framework.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -32,6 +34,8 @@
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
import android.service.notification.StatusBarNotification;
import android.support.test.filters.SmallTest;
import android.testing.AndroidTestingRunner;
@@ -41,14 +45,14 @@
import android.widget.Button;
import android.widget.LinearLayout;
-import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.R;
import com.android.systemui.SysuiTestCase;
-import com.android.systemui.statusbar.notification.NotificationData;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.plugins.ActivityStarter.OnDismissAction;
import com.android.systemui.statusbar.SmartReplyController;
+import com.android.systemui.statusbar.notification.NotificationData;
import com.android.systemui.statusbar.phone.KeyguardDismissUtil;
-
-import java.util.concurrent.atomic.AtomicReference;
+import com.android.systemui.statusbar.phone.ShadeController;
import org.junit.After;
import org.junit.Before;
@@ -57,6 +61,10 @@
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
@SmallTest
@@ -67,6 +75,10 @@
private static final String[] TEST_CHOICES = new String[]{"Hello", "What's up?", "I'm here"};
private static final String TEST_NOTIFICATION_KEY = "akey";
+ private static final String[] TEST_ACTION_TITLES = new String[]{
+ "First action", "Open something", "Action"
+ };
+
private static final int WIDTH_SPEC = MeasureSpec.makeMeasureSpec(500, MeasureSpec.EXACTLY);
private static final int HEIGHT_SPEC = MeasureSpec.makeMeasureSpec(400, MeasureSpec.AT_MOST);
@@ -74,6 +86,8 @@
private SmartReplyView mView;
private View mContainer;
+ private Icon mActionIcon;
+
private int mSingleLinePaddingHorizontal;
private int mDoubleLinePaddingHorizontal;
private int mSpacing;
@@ -82,12 +96,16 @@
private NotificationData.Entry mEntry;
private Notification mNotification;
+ @Mock ActivityStarter mActivityStarter;
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
mReceiver = new BlockingQueueIntentReceiver();
mContext.registerReceiver(mReceiver, new IntentFilter(TEST_ACTION));
mDependency.get(KeyguardDismissUtil.class).setDismissHandler(action -> action.onDismiss());
+ mDependency.injectMockDependency(ShadeController.class);
+ mDependency.injectTestDependency(ActivityStarter.class, mActivityStarter);
mContainer = new View(mContext, null);
mView = SmartReplyView.inflate(mContext, null);
@@ -108,6 +126,8 @@
when(sbn.getNotification()).thenReturn(mNotification);
when(sbn.getKey()).thenReturn(TEST_NOTIFICATION_KEY);
mEntry = new NotificationData.Entry(sbn);
+
+ mActionIcon = Icon.createWithResource(mContext, R.drawable.ic_person);
}
@After
@@ -117,7 +137,7 @@
@Test
public void testSendSmartReply_intentContainsResultsAndSource() throws InterruptedException {
- setRepliesFromRemoteInput(TEST_CHOICES);
+ setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
@@ -130,7 +150,7 @@
@Test
public void testSendSmartReply_keyguardCancelled() throws InterruptedException {
mDependency.get(KeyguardDismissUtil.class).setDismissHandler(action -> {});
- setRepliesFromRemoteInput(TEST_CHOICES);
+ setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
@@ -141,7 +161,7 @@
public void testSendSmartReply_waitsForKeyguard() throws InterruptedException {
AtomicReference<OnDismissAction> actionRef = new AtomicReference<>();
mDependency.get(KeyguardDismissUtil.class).setDismissHandler(actionRef::set);
- setRepliesFromRemoteInput(TEST_CHOICES);
+ setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
@@ -159,7 +179,7 @@
@Test
public void testSendSmartReply_controllerCalled() {
- setRepliesFromRemoteInput(TEST_CHOICES);
+ setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
verify(mLogger).smartReplySent(mEntry, 2, TEST_CHOICES[2]);
}
@@ -167,7 +187,7 @@
@Test
public void testSendSmartReply_hidesContainer() {
mContainer.setVisibility(View.VISIBLE);
- setRepliesFromRemoteInput(TEST_CHOICES);
+ setSmartReplies(TEST_CHOICES);
mView.getChildAt(0).performClick();
assertEquals(View.GONE, mContainer.getVisibility());
}
@@ -198,7 +218,7 @@
ViewGroup expectedView = buildExpectedView(choices, 1);
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- setRepliesFromRemoteInput(choices);
+ setSmartReplies(choices);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
assertEqualMeasures(expectedView, mView);
@@ -217,7 +237,7 @@
expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
10 + expectedView.getMeasuredHeight());
- setRepliesFromRemoteInput(choices);
+ setSmartReplies(choices);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
mView.layout(10, 10, 10 + mView.getMeasuredWidth(), 10 + mView.getMeasuredHeight());
@@ -235,7 +255,7 @@
ViewGroup expectedView = buildExpectedView(choices, 2);
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- setRepliesFromRemoteInput(choices);
+ setSmartReplies(choices);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
assertEqualMeasures(expectedView, mView);
@@ -254,7 +274,7 @@
expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
10 + expectedView.getMeasuredHeight());
- setRepliesFromRemoteInput(choices);
+ setSmartReplies(choices);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
mView.layout(10, 10, 10 + mView.getMeasuredWidth(), 10 + mView.getMeasuredHeight());
@@ -273,7 +293,7 @@
ViewGroup expectedView = buildExpectedView(new CharSequence[]{"Hi", "Bye"}, 1);
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- setRepliesFromRemoteInput(choices);
+ setSmartReplies(choices);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
assertEqualMeasures(expectedView, mView);
@@ -293,7 +313,7 @@
expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
10 + expectedView.getMeasuredHeight());
- setRepliesFromRemoteInput(choices);
+ setSmartReplies(choices);
mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
mView.layout(10, 10, 10 + mView.getMeasuredWidth(), 10 + mView.getMeasuredHeight());
@@ -313,7 +333,7 @@
new CharSequence[]{"Short", "Short", "Looooooong \nreplyyyyy"}, 2);
expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
- setRepliesFromRemoteInput(choices);
+ setSmartReplies(choices);
mView.measure(
MeasureSpec.makeMeasureSpec(expectedView.getMeasuredWidth(), MeasureSpec.AT_MOST),
MeasureSpec.UNSPECIFIED);
@@ -335,7 +355,7 @@
expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
10 + expectedView.getMeasuredHeight());
- setRepliesFromRemoteInput(choices);
+ setSmartReplies(choices);
mView.measure(
MeasureSpec.makeMeasureSpec(expectedView.getMeasuredWidth(), MeasureSpec.AT_MOST),
MeasureSpec.UNSPECIFIED);
@@ -359,7 +379,7 @@
expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
10 + expectedView.getMeasuredHeight());
- setRepliesFromRemoteInput(choices);
+ setSmartReplies(choices);
mView.measure(
MeasureSpec.makeMeasureSpec(expectedView.getMeasuredWidth(), MeasureSpec.AT_MOST),
MeasureSpec.UNSPECIFIED);
@@ -371,15 +391,45 @@
assertReplyButtonHidden(mView.getChildAt(2));
}
- private void setRepliesFromRemoteInput(CharSequence[] choices) {
+ private void setSmartReplies(CharSequence[] choices) {
PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
new Intent(TEST_ACTION), 0);
RemoteInput input = new RemoteInput.Builder(TEST_RESULT_KEY).setChoices(choices).build();
- mView.setRepliesFromRemoteInput(input, pendingIntent, mLogger, mEntry, mContainer, choices);
+ mView.resetSmartSuggestions(mContainer);
+ mView.addRepliesFromRemoteInput(input, pendingIntent, mLogger, mEntry, choices);
+ }
+
+ private Notification.Action createAction(String actionTitle) {
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0,
+ new Intent(TEST_ACTION), 0);
+ return new Notification.Action.Builder(mActionIcon, actionTitle, pendingIntent).build();
+ }
+
+ private List<Notification.Action> createActions(String[] actionTitles) {
+ List<Notification.Action> actions = new ArrayList<>();
+ for (String title : actionTitles) {
+ actions.add(createAction(title));
+ }
+ return actions;
+ }
+
+ private void setSmartActions(String[] actionTitles) {
+ mView.resetSmartSuggestions(mContainer);
+ mView.addSmartActions(createActions(actionTitles));
+ }
+
+ private void setSmartRepliesAndActions(CharSequence[] choices, String[] actionTitles) {
+ setSmartReplies(choices);
+ mView.addSmartActions(createActions(actionTitles));
+ }
+
+ private ViewGroup buildExpectedView(CharSequence[] choices, int lineCount) {
+ return buildExpectedView(choices, lineCount, new ArrayList<>());
}
/** Builds a {@link ViewGroup} whose measures and layout mirror a {@link SmartReplyView}. */
- private ViewGroup buildExpectedView(CharSequence[] choices, int lineCount) {
+ private ViewGroup buildExpectedView(
+ CharSequence[] choices, int lineCount, List<Notification.Action> actions) {
LinearLayout layout = new LinearLayout(mContext);
layout.setOrientation(LinearLayout.HORIZONTAL);
@@ -401,6 +451,7 @@
return null;
}
+ // Add smart replies
Button previous = null;
for (int i = 0; i < choices.length; ++i) {
Button current = mView.inflateReplyButton(mContext, mView, i, choices[i],
@@ -420,6 +471,24 @@
previous = current;
}
+ // Add smart actions
+ for (int i = 0; i < actions.size(); ++i) {
+ Button current = inflateActionButton(actions.get(i));
+ current.setPadding(paddingHorizontal, current.getPaddingTop(), paddingHorizontal,
+ current.getPaddingBottom());
+ if (previous != null) {
+ ViewGroup.MarginLayoutParams lp =
+ (ViewGroup.MarginLayoutParams) previous.getLayoutParams();
+ if (isRtl) {
+ lp.leftMargin = mSpacing;
+ } else {
+ lp.rightMargin = mSpacing;
+ }
+ }
+ layout.addView(current);
+ previous = current;
+ }
+
return layout;
}
@@ -455,4 +524,255 @@
assertEquals(expected.getPaddingRight(), actual.getPaddingRight());
assertEquals(expected.getPaddingBottom(), actual.getPaddingBottom());
}
+
+
+ // =============================================================================================
+ // ============================= Smart Action tests ============================================
+ // =============================================================================================
+
+ @Test
+ public void testTapSmartAction_waitsForKeyguard() throws InterruptedException {
+ setSmartActions(TEST_ACTION_TITLES);
+
+ mView.getChildAt(2).performClick();
+
+ verify(mActivityStarter, times(1)).startPendingIntentDismissingKeyguard(any());
+ }
+
+ @Test
+ public void testMeasure_shortSmartActions() {
+ String[] actions = new String[] {"Hi", "Hello", "Bye"};
+ // All choices should be displayed as SINGLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(new CharSequence[0], 1, createActions(actions));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ setSmartActions(actions);
+ mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ assertEqualMeasures(expectedView, mView);
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(1), mView.getChildAt(1));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(2), mView.getChildAt(2));
+ }
+
+ @Test
+ public void testLayout_shortSmartActions() {
+ String[] actions = new String[] {"Hi", "Hello", "Bye"};
+ // All choices should be displayed as SINGLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(new CharSequence[0], 1, createActions(actions));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
+ 10 + expectedView.getMeasuredHeight());
+
+ setSmartActions(actions);
+ mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ mView.layout(10, 10, 10 + mView.getMeasuredWidth(), 10 + mView.getMeasuredHeight());
+
+ assertEqualLayouts(expectedView, mView);
+ assertEqualLayouts(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertEqualLayouts(expectedView.getChildAt(1), mView.getChildAt(1));
+ assertEqualLayouts(expectedView.getChildAt(2), mView.getChildAt(2));
+ }
+
+ @Test
+ public void testMeasure_smartActionWithTwoLines() {
+ String[] actions = new String[] {"Hi", "Hello\neveryone", "Bye"};
+
+ // All actions should be displayed as DOUBLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(new CharSequence[0], 2, createActions(actions));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ setSmartActions(actions);
+ mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ assertEqualMeasures(expectedView, mView);
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(1), mView.getChildAt(1));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(2), mView.getChildAt(2));
+ }
+
+ @Test
+ public void testLayout_smartActionWithTwoLines() {
+ String[] actions = new String[] {"Hi", "Hello\neveryone", "Bye"};
+
+ // All actions should be displayed as DOUBLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(new CharSequence[0], 2, createActions(actions));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
+ 10 + expectedView.getMeasuredHeight());
+
+ setSmartActions(actions);
+ mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ mView.layout(10, 10, 10 + mView.getMeasuredWidth(), 10 + mView.getMeasuredHeight());
+
+ assertEqualLayouts(expectedView, mView);
+ assertEqualLayouts(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertEqualLayouts(expectedView.getChildAt(1), mView.getChildAt(1));
+ assertEqualLayouts(expectedView.getChildAt(2), mView.getChildAt(2));
+ }
+
+ @Test
+ public void testMeasure_smartActionWithThreeLines() {
+ String[] actions = new String[] {"Hi", "Hello\nevery\nbody", "Bye"};
+
+ // The action with three lines should NOT be displayed. All other actions should be
+ // displayed as SINGLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(new CharSequence[0], 1,
+ createActions(new String[]{"Hi", "Bye"}));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ setSmartActions(actions);
+ mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ assertEqualMeasures(expectedView, mView);
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertReplyButtonHidden(mView.getChildAt(1));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(1), mView.getChildAt(2));
+ }
+
+ @Test
+ public void testLayout_smartActionWithThreeLines() {
+ String[] actions = new String[] {"Hi", "Hello\nevery\nbody", "Bye"};
+
+ // The action with three lines should NOT be displayed. All other actions should be
+ // displayed as SINGLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(new CharSequence[0], 1,
+ createActions(new String[]{"Hi", "Bye"}));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
+ 10 + expectedView.getMeasuredHeight());
+
+ setSmartActions(actions);
+ mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ mView.layout(10, 10, 10 + mView.getMeasuredWidth(), 10 + mView.getMeasuredHeight());
+
+ assertEqualLayouts(expectedView, mView);
+ assertEqualLayouts(expectedView.getChildAt(0), mView.getChildAt(0));
+ // We don't care about mView.getChildAt(1)'s layout because it's hidden (see
+ // testMeasure_smartActionWithThreeLines).
+ assertEqualLayouts(expectedView.getChildAt(1), mView.getChildAt(2));
+ }
+
+ @Test
+ public void testMeasure_squeezeLongestSmartAction() {
+ String[] actions = new String[] {"Short", "Short", "Looooooong replyyyyy"};
+
+ // All actions should be displayed as DOUBLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(new CharSequence[0], 2,
+ createActions(new String[] {"Short", "Short", "Looooooong \nreplyyyyy"}));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ setSmartActions(actions);
+ mView.measure(
+ MeasureSpec.makeMeasureSpec(expectedView.getMeasuredWidth(), MeasureSpec.AT_MOST),
+ MeasureSpec.UNSPECIFIED);
+
+ assertEqualMeasures(expectedView, mView);
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(1), mView.getChildAt(1));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(2), mView.getChildAt(2));
+ }
+
+ @Test
+ public void testLayout_squeezeLongestSmartAction() {
+ String[] actions = new String[] {"Short", "Short", "Looooooong replyyyyy"};
+
+ // All actions should be displayed as DOUBLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(new CharSequence[0], 2,
+ createActions(new String[] {"Short", "Short", "Looooooong \nreplyyyyy"}));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
+ 10 + expectedView.getMeasuredHeight());
+
+ setSmartActions(actions);
+ mView.measure(
+ MeasureSpec.makeMeasureSpec(expectedView.getMeasuredWidth(), MeasureSpec.AT_MOST),
+ MeasureSpec.UNSPECIFIED);
+ mView.layout(10, 10, 10 + mView.getMeasuredWidth(), 10 + mView.getMeasuredHeight());
+
+ assertEqualLayouts(expectedView, mView);
+ assertEqualLayouts(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertEqualLayouts(expectedView.getChildAt(1), mView.getChildAt(1));
+ assertEqualLayouts(expectedView.getChildAt(2), mView.getChildAt(2));
+ }
+
+ @Test
+ public void testMeasure_dropLongestSmartAction() {
+ String[] actions = new String[] {"Short", "Short", "LooooooongUnbreakableReplyyyyy"};
+
+ // Short actions should be shown as single line views
+ ViewGroup expectedView = buildExpectedView(
+ new CharSequence[0], 1, createActions(new String[] {"Short", "Short"}));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+ expectedView.layout(10, 10, 10 + expectedView.getMeasuredWidth(),
+ 10 + expectedView.getMeasuredHeight());
+
+ setSmartActions(actions);
+ mView.measure(
+ MeasureSpec.makeMeasureSpec(expectedView.getMeasuredWidth(), MeasureSpec.AT_MOST),
+ MeasureSpec.UNSPECIFIED);
+ mView.layout(10, 10, 10 + mView.getMeasuredWidth(), 10 + mView.getMeasuredHeight());
+
+ assertEqualLayouts(expectedView, mView);
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(1), mView.getChildAt(1));
+ assertReplyButtonHidden(mView.getChildAt(2));
+ }
+
+ private Button inflateActionButton(Notification.Action action) {
+ return mView.inflateActionButton(getContext(), mView, action);
+ }
+
+ @Test
+ public void testInflateActionButton_smartActionIconSingleLineSizeForTwoLineButton() {
+ // Ensure smart action icons are the same size regardless of the number of text rows in the
+ // button.
+ Button singleLineButton = inflateActionButton(createAction("One line"));
+ Button doubleLineButton = inflateActionButton(createAction("Two\nlines"));
+ Drawable singleLineDrawable = singleLineButton.getCompoundDrawables()[0]; // left drawable
+ Drawable doubleLineDrawable = doubleLineButton.getCompoundDrawables()[0]; // left drawable
+ assertEquals(singleLineDrawable.getBounds().width(),
+ doubleLineDrawable.getBounds().width());
+ assertEquals(singleLineDrawable.getBounds().height(),
+ doubleLineDrawable.getBounds().height());
+ }
+
+ @Test
+ public void testMeasure_shortChoicesAndActions() {
+ CharSequence[] choices = new String[] {"Hi", "Hello"};
+ String[] actions = new String[] {"Bye"};
+ // All choices should be displayed as SINGLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(choices, 1, createActions(actions));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ setSmartRepliesAndActions(choices, actions);
+ mView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ assertEqualMeasures(expectedView, mView);
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(1), mView.getChildAt(1));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(2), mView.getChildAt(2));
+ }
+
+ @Test
+ public void testMeasure_choicesAndActionsSqueezeLongestAction() {
+ CharSequence[] choices = new String[] {"Short", "Short"};
+ String[] actions = new String[] {"Looooooong replyyyyy"};
+
+ // All actions should be displayed as DOUBLE-line smart action buttons.
+ ViewGroup expectedView = buildExpectedView(choices, 2,
+ createActions(new String[] {"Looooooong \nreplyyyyy"}));
+ expectedView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+ setSmartRepliesAndActions(choices, actions);
+ mView.measure(
+ MeasureSpec.makeMeasureSpec(expectedView.getMeasuredWidth(), MeasureSpec.AT_MOST),
+ MeasureSpec.UNSPECIFIED);
+
+ assertEqualMeasures(expectedView, mView);
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(0), mView.getChildAt(0));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(1), mView.getChildAt(1));
+ assertReplyButtonShownWithEqualMeasures(expectedView.getChildAt(2), mView.getChildAt(2));
+ }
}