Merge "Add smart actions to message notifications."
This commit is contained in:
committed by
Android (Google) Code Review
commit
fa830752aa
@@ -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.
|
||||
*
|
||||
|
||||
34
packages/SystemUI/res/layout/smart_action_button.xml
Normal file
34
packages/SystemUI/res/layout/smart_action_button.xml
Normal 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"/>
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user