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));
+    }
 }