Merge "Add smart actions to message notifications."

This commit is contained in:
Gustav Sennton
2018-11-22 00:58:46 +00:00
committed by Android (Google) Code Review
7 changed files with 796 additions and 68 deletions

View File

@@ -3192,6 +3192,25 @@ public class Notification implements Parcelable
return null;
}
/**
* 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.
*

View File

@@ -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"/>

View File

@@ -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

View File

@@ -55,6 +55,8 @@ import com.android.systemui.statusbar.policy.RemoteInputView;
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 @@ public class NotificationContentView extends FrameLayout {
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 @@ public class NotificationContentView extends FrameLayout {
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 @@ public class NotificationContentView extends FrameLayout {
}
}
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;

View File

@@ -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.TextPaint;
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.annotations.VisibleForTesting;
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 com.android.systemui.statusbar.phone.KeyguardDismissUtil;
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 @@ public class SmartReplyView extends ViewGroup {
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 @@ public class SmartReplyView extends ViewGroup {
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 @@ public class SmartReplyView extends ViewGroup {
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 @@ public class SmartReplyView extends ViewGroup {
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 @@ public class SmartReplyView extends ViewGroup {
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 @@ public class SmartReplyView extends ViewGroup {
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 @@ public class SmartReplyView extends ViewGroup {
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 @@ public class SmartReplyView extends ViewGroup {
button.setTextColor(textColor);
}
private ActivityStarter getActivityStarter() {
if (mActivityStarter == null) {
mActivityStarter = Dependency.get(ActivityStarter.class);
}
return mActivityStarter;
}
@VisibleForTesting
static class LayoutParams extends ViewGroup.LayoutParams {

View File

@@ -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.verify;
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 @@ public class NotificationContentViewTest extends SysuiTestCase {
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 @@ public class NotificationContentViewTest extends SysuiTestCase {
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 @@ public class NotificationContentViewTest extends SysuiTestCase {
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;
}
}

View File

@@ -22,7 +22,9 @@ import static junit.framework.Assert.assertNull;
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.app.RemoteInput;
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.view.ViewGroup;
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.junit.runner.RunWith;
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
private SmartReplyView mView;
private View mContainer;
private Icon mActionIcon;
private int mSingleLinePaddingHorizontal;
private int mDoubleLinePaddingHorizontal;
private int mSpacing;
@@ -82,12 +96,16 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
@Test
public void testSendSmartReply_intentContainsResultsAndSource() throws InterruptedException {
setRepliesFromRemoteInput(TEST_CHOICES);
setSmartReplies(TEST_CHOICES);
mView.getChildAt(2).performClick();
@@ -130,7 +150,7 @@ public class SmartReplyViewTest extends SysuiTestCase {
@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 class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
@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 @@ public class SmartReplyViewTest extends SysuiTestCase {
@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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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 @@ public class SmartReplyViewTest extends SysuiTestCase {
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));
}
}