Loading core/java/android/app/Notification.java +19 −0 Original line number Diff line number Diff line Loading @@ -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. * Loading packages/SystemUI/res/layout/smart_action_button.xml 0 → 100644 +34 −0 Original line number Diff line number Diff line <!-- ~ 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"/> packages/SystemUI/res/values/dimens.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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 Loading packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +94 −36 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -1285,38 +1287,88 @@ public class NotificationContentView extends FrameLayout { return; } Notification notification = entry.notification.getNotification(); SmartRepliesAndActions smartRepliesAndActions = chooseSmartRepliesAndActions( mSmartReplyConstants, entry); applyRemoteInput(entry, smartRepliesAndActions.freeformRemoteInputActionPair != null); applySmartReplyView(smartRepliesAndActions, entry); } /** * 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)); Notification notification = entry.notification.getNotification(); Pair<RemoteInput, Notification.Action> remoteInputActionPair = entry.notification.getNotification().findRemoteInputActionPair(false /*freeform */); notification.findRemoteInputActionPair(false /* freeform */); Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair = notification.findRemoteInputActionPair(true /* freeform */); boolean enableAppGeneratedSmartReplies = (mSmartReplyConstants.isEnabled() && (!mSmartReplyConstants.requiresTargetingP() || entry.targetSdk >= Build.VERSION_CODES.P)); RemoteInput remoteInputWithChoices = null; PendingIntent pendingIntentWithChoices= null; CharSequence[] choices = null; if (enableAppGeneratedSmartReplies boolean appGeneratedSmartRepliesExist = enableAppGeneratedSmartReplies && remoteInputActionPair != null && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())) { // app generated smart replies remoteInputWithChoices = remoteInputActionPair.first; pendingIntentWithChoices = remoteInputActionPair.second.actionIntent; choices = remoteInputActionPair.first.getChoices(); && !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); } applyRemoteInput(entry, freeformRemoteInputActionPair != null); applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices, entry, choices); @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; } boolean smartRepliesExist() { return remoteInputWithChoices != null && pendingIntentForSmartReplies != null && !ArrayUtils.isEmpty(smartReplies); } } private void applyRemoteInput(NotificationData.Entry entry, boolean hasFreeformRemoteInput) { Loading Loading @@ -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; } Loading Loading @@ -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; Loading packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java +104 −9 Original line number Diff line number Diff line 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; Loading @@ -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; Loading @@ -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; Loading @@ -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 = Loading Loading @@ -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); Loading Loading @@ -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) { Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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) { Loading Loading @@ -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) { Loading @@ -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(); Loading Loading @@ -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 { Loading Loading
core/java/android/app/Notification.java +19 −0 Original line number Diff line number Diff line Loading @@ -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. * Loading
packages/SystemUI/res/layout/smart_action_button.xml 0 → 100644 +34 −0 Original line number Diff line number Diff line <!-- ~ 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"/>
packages/SystemUI/res/values/dimens.xml +1 −0 Original line number Diff line number Diff line Loading @@ -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 Loading
packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +94 −36 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -1285,38 +1287,88 @@ public class NotificationContentView extends FrameLayout { return; } Notification notification = entry.notification.getNotification(); SmartRepliesAndActions smartRepliesAndActions = chooseSmartRepliesAndActions( mSmartReplyConstants, entry); applyRemoteInput(entry, smartRepliesAndActions.freeformRemoteInputActionPair != null); applySmartReplyView(smartRepliesAndActions, entry); } /** * 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)); Notification notification = entry.notification.getNotification(); Pair<RemoteInput, Notification.Action> remoteInputActionPair = entry.notification.getNotification().findRemoteInputActionPair(false /*freeform */); notification.findRemoteInputActionPair(false /* freeform */); Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair = notification.findRemoteInputActionPair(true /* freeform */); boolean enableAppGeneratedSmartReplies = (mSmartReplyConstants.isEnabled() && (!mSmartReplyConstants.requiresTargetingP() || entry.targetSdk >= Build.VERSION_CODES.P)); RemoteInput remoteInputWithChoices = null; PendingIntent pendingIntentWithChoices= null; CharSequence[] choices = null; if (enableAppGeneratedSmartReplies boolean appGeneratedSmartRepliesExist = enableAppGeneratedSmartReplies && remoteInputActionPair != null && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())) { // app generated smart replies remoteInputWithChoices = remoteInputActionPair.first; pendingIntentWithChoices = remoteInputActionPair.second.actionIntent; choices = remoteInputActionPair.first.getChoices(); && !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); } applyRemoteInput(entry, freeformRemoteInputActionPair != null); applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices, entry, choices); @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; } boolean smartRepliesExist() { return remoteInputWithChoices != null && pendingIntentForSmartReplies != null && !ArrayUtils.isEmpty(smartReplies); } } private void applyRemoteInput(NotificationData.Entry entry, boolean hasFreeformRemoteInput) { Loading Loading @@ -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; } Loading Loading @@ -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; Loading
packages/SystemUI/src/com/android/systemui/statusbar/policy/SmartReplyView.java +104 −9 Original line number Diff line number Diff line 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; Loading @@ -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; Loading @@ -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; Loading @@ -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 = Loading Loading @@ -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); Loading Loading @@ -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) { Loading @@ -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); Loading Loading @@ -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); Loading Loading @@ -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) { Loading Loading @@ -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) { Loading @@ -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(); Loading Loading @@ -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 { Loading