Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit fa830752 authored by Gustav Sennton's avatar Gustav Sennton Committed by Android (Google) Code Review
Browse files

Merge "Add smart actions to message notifications."

parents 0a889e3f eab5368d
Loading
Loading
Loading
Loading
+19 −0
Original line number Diff line number Diff line
@@ -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 −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"/>
+1 −0
Original line number Diff line number Diff line
@@ -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
+94 −36
Original line number Diff line number Diff line
@@ -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);

        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) {
@@ -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;
+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;
@@ -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 {

Loading