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

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

Merge "Refactor smart reply inflation functionality into its own class."

parents 586d0f33 b944ce5c
Loading
Loading
Loading
Loading
+6 −117
Original line number Diff line number Diff line
@@ -19,7 +19,6 @@ package com.android.systemui.statusbar.notification.row;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.RemoteInput;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
@@ -28,7 +27,6 @@ import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.NotificationHeaderView;
import android.view.View;
@@ -39,7 +37,6 @@ import android.widget.ImageView;
import android.widget.LinearLayout;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ContrastColorUtil;
import com.android.systemui.Dependency;
import com.android.systemui.R;
@@ -52,13 +49,14 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper;
import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.InflatedSmartReplies;
import com.android.systemui.statusbar.policy.InflatedSmartReplies.SmartRepliesAndActions;
import com.android.systemui.statusbar.policy.RemoteInputView;
import com.android.systemui.statusbar.policy.SmartReplyConstants;
import com.android.systemui.statusbar.policy.SmartReplyView;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.List;

/**
 * A frame layout containing the actual payload of the notification, including the contracted,
@@ -1319,7 +1317,7 @@ public class NotificationContentView extends FrameLayout {
        }

        SmartRepliesAndActions smartRepliesAndActions =
                chooseSmartRepliesAndActions(mSmartReplyConstants, entry);
                InflatedSmartReplies.chooseSmartRepliesAndActions(mSmartReplyConstants, entry);
        if (DEBUG) {
            Log.d(TAG, String.format("Adding suggestions for %s, %d actions, and %d replies.",
                    entry.notification.getKey(),
@@ -1328,86 +1326,10 @@ public class NotificationContentView extends FrameLayout {
                    smartRepliesAndActions.smartReplies == null ? 0 :
                            smartRepliesAndActions.smartReplies.choices.length));
        }

        applyRemoteInput(entry, smartRepliesAndActions.hasFreeformRemoteInput);
        applyRemoteInput(entry, InflatedSmartReplies.hasFreeformRemoteInput(entry));
        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 NotificationEntry entry) {
        Notification notification = entry.notification.getNotification();
        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
                notification.findRemoteInputActionPair(false /* freeform */);
        Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair =
                notification.findRemoteInputActionPair(true /* freeform */);

        if (!smartReplyConstants.isEnabled()) {
            if (DEBUG) {
                Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for "
                        + entry.notification.getKey());
            }
            return new SmartRepliesAndActions(null, null, freeformRemoteInputActionPair != null);
        }
        // Only use smart replies from the app if they target P or above. We have this check because
        // the smart reply API has been used for other things (Wearables) in the past. The API to
        // add smart actions is new in Q so it doesn't require a target-sdk check.
        boolean enableAppGeneratedSmartReplies = (!smartReplyConstants.requiresTargetingP()
                || entry.targetSdk >= Build.VERSION_CODES.P);

        boolean appGeneratedSmartRepliesExist =
                enableAppGeneratedSmartReplies
                        && remoteInputActionPair != null
                        && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())
                        && remoteInputActionPair.second.actionIntent != null;

        List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions();
        boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty();

        SmartReplyView.SmartReplies smartReplies = null;
        SmartReplyView.SmartActions smartActions = null;
        if (appGeneratedSmartRepliesExist) {
            smartReplies = new SmartReplyView.SmartReplies(
                    remoteInputActionPair.first.getChoices(),
                    remoteInputActionPair.first,
                    remoteInputActionPair.second.actionIntent,
                    false /* fromAssistant */);
        }
        if (appGeneratedSmartActionsExist) {
            smartActions = new SmartReplyView.SmartActions(appGeneratedSmartActions,
                    false /* fromAssistant */);
        }
        // Apps didn't provide any smart replies / actions, use those from NAS (if any).
        if (!appGeneratedSmartRepliesExist && !appGeneratedSmartActionsExist) {
            boolean useGeneratedReplies = !ArrayUtils.isEmpty(entry.systemGeneratedSmartReplies)
                    && freeformRemoteInputActionPair != null
                    && freeformRemoteInputActionPair.second.getAllowGeneratedReplies()
                    && freeformRemoteInputActionPair.second.actionIntent != null;
            if (useGeneratedReplies) {
                smartReplies = new SmartReplyView.SmartReplies(
                        entry.systemGeneratedSmartReplies,
                        freeformRemoteInputActionPair.first,
                        freeformRemoteInputActionPair.second.actionIntent,
                        true /* fromAssistant */);
            }
            boolean useSmartActions = !ArrayUtils.isEmpty(entry.systemGeneratedSmartActions)
                    && notification.getAllowSystemGeneratedContextualActions();
            if (useSmartActions) {
                smartActions = new SmartReplyView.SmartActions(
                        entry.systemGeneratedSmartActions, true /* fromAssistant */);
            }
        }
        return new SmartRepliesAndActions(
                smartReplies, smartActions, freeformRemoteInputActionPair != null);
    }

    private void applyRemoteInput(NotificationEntry entry, boolean hasFreeformRemoteInput) {
        View bigContentView = mExpandedChild;
        if (bigContentView != null) {
@@ -1546,26 +1468,11 @@ public class NotificationContentView extends FrameLayout {
            return null;
        }
        LinearLayout smartReplyContainer = (LinearLayout) smartReplyContainerCandidate;
        // If there are no smart replies and no smart actions - early out.
        if (smartRepliesAndActions.smartReplies == null
                && smartRepliesAndActions.smartActions == null) {
            smartReplyContainer.setVisibility(View.GONE);
            return null;
        }
        // If we are showing the spinner we don't want to add the buttons.
        boolean showingSpinner = entry.notification.getNotification()
                .extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
        if (showingSpinner) {
            smartReplyContainer.setVisibility(View.GONE);
            return null;
        }
        // If we are keeping the notification around while sending we don't want to add the buttons.
        boolean hideSmartReplies = entry.notification.getNotification()
                .extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false);
        if (hideSmartReplies) {
        if (!InflatedSmartReplies.shouldShowSmartReplyView(entry, smartRepliesAndActions)) {
            smartReplyContainer.setVisibility(View.GONE);
            return null;
        }

        SmartReplyView smartReplyView = null;
        if (smartReplyContainer.getChildCount() == 0) {
            smartReplyView = SmartReplyView.inflate(mContext, smartReplyContainer);
@@ -2005,22 +1912,4 @@ public class NotificationContentView extends FrameLayout {
        }
        pw.println();
    }

    @VisibleForTesting
    static class SmartRepliesAndActions {
        @Nullable
        public final SmartReplyView.SmartReplies smartReplies;
        @Nullable
        public final SmartReplyView.SmartActions smartActions;
        public final boolean hasFreeformRemoteInput;

        SmartRepliesAndActions(
                @Nullable SmartReplyView.SmartReplies smartReplies,
                @Nullable SmartReplyView.SmartActions smartActions,
                boolean hasFreeformRemoteInput) {
            this.smartReplies = smartReplies;
            this.smartActions = smartActions;
            this.hasFreeformRemoteInput = hasFreeformRemoteInput;
        }
    }
}
+166 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 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.
 */

package com.android.systemui.statusbar.policy;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.RemoteInput;
import android.os.Build;
import android.util.Log;
import android.util.Pair;

import com.android.internal.util.ArrayUtils;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;

import java.util.List;

/**
 * Holder for inflated smart replies and actions. These objects should be inflated on a background
 * thread, to later be accessed and modified on the (performance critical) UI thread.
 */
public class InflatedSmartReplies {
    private static final String TAG = "InflatedSmartReplies";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private InflatedSmartReplies() { }

    /**
     * Returns whether we should show the smart reply view and its smart suggestions.
     */
    public static boolean shouldShowSmartReplyView(
            NotificationEntry entry,
            SmartRepliesAndActions smartRepliesAndActions) {
        if (smartRepliesAndActions.smartReplies == null
                && smartRepliesAndActions.smartActions == null) {
            // There are no smart replies and no smart actions.
            return false;
        }
        // If we are showing the spinner we don't want to add the buttons.
        boolean showingSpinner = entry.notification.getNotification()
                .extras.getBoolean(Notification.EXTRA_SHOW_REMOTE_INPUT_SPINNER, false);
        if (showingSpinner) {
            return false;
        }
        // If we are keeping the notification around while sending we don't want to add the buttons.
        boolean hideSmartReplies = entry.notification.getNotification()
                .extras.getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false);
        if (hideSmartReplies) {
            return false;
        }
        return true;
    }

    /**
     * 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.
     */
    @NonNull
    public static SmartRepliesAndActions chooseSmartRepliesAndActions(
            SmartReplyConstants smartReplyConstants,
            final NotificationEntry entry) {
        Notification notification = entry.notification.getNotification();
        Pair<RemoteInput, Notification.Action> remoteInputActionPair =
                notification.findRemoteInputActionPair(false /* freeform */);
        Pair<RemoteInput, Notification.Action> freeformRemoteInputActionPair =
                notification.findRemoteInputActionPair(true /* freeform */);

        if (!smartReplyConstants.isEnabled()) {
            if (DEBUG) {
                Log.d(TAG, "Smart suggestions not enabled, not adding suggestions for "
                        + entry.notification.getKey());
            }
            return new SmartRepliesAndActions(null, null);
        }
        // Only use smart replies from the app if they target P or above. We have this check because
        // the smart reply API has been used for other things (Wearables) in the past. The API to
        // add smart actions is new in Q so it doesn't require a target-sdk check.
        boolean enableAppGeneratedSmartReplies = (!smartReplyConstants.requiresTargetingP()
                || entry.targetSdk >= Build.VERSION_CODES.P);

        boolean appGeneratedSmartRepliesExist =
                enableAppGeneratedSmartReplies
                        && remoteInputActionPair != null
                        && !ArrayUtils.isEmpty(remoteInputActionPair.first.getChoices())
                        && remoteInputActionPair.second.actionIntent != null;

        List<Notification.Action> appGeneratedSmartActions = notification.getContextualActions();
        boolean appGeneratedSmartActionsExist = !appGeneratedSmartActions.isEmpty();

        SmartReplyView.SmartReplies smartReplies = null;
        SmartReplyView.SmartActions smartActions = null;
        if (appGeneratedSmartRepliesExist) {
            smartReplies = new SmartReplyView.SmartReplies(
                    remoteInputActionPair.first.getChoices(),
                    remoteInputActionPair.first,
                    remoteInputActionPair.second.actionIntent,
                    false /* fromAssistant */);
        }
        if (appGeneratedSmartActionsExist) {
            smartActions = new SmartReplyView.SmartActions(appGeneratedSmartActions,
                    false /* fromAssistant */);
        }
        // Apps didn't provide any smart replies / actions, use those from NAS (if any).
        if (!appGeneratedSmartRepliesExist && !appGeneratedSmartActionsExist) {
            boolean useGeneratedReplies = !ArrayUtils.isEmpty(entry.systemGeneratedSmartReplies)
                    && freeformRemoteInputActionPair != null
                    && freeformRemoteInputActionPair.second.getAllowGeneratedReplies()
                    && freeformRemoteInputActionPair.second.actionIntent != null;
            if (useGeneratedReplies) {
                smartReplies = new SmartReplyView.SmartReplies(
                        entry.systemGeneratedSmartReplies,
                        freeformRemoteInputActionPair.first,
                        freeformRemoteInputActionPair.second.actionIntent,
                        true /* fromAssistant */);
            }
            boolean useSmartActions = !ArrayUtils.isEmpty(entry.systemGeneratedSmartActions)
                    && notification.getAllowSystemGeneratedContextualActions();
            if (useSmartActions) {
                smartActions = new SmartReplyView.SmartActions(
                        entry.systemGeneratedSmartActions, true /* fromAssistant */);
            }
        }
        return new SmartRepliesAndActions(smartReplies, smartActions);
    }

    /**
     * Returns whether the {@link Notification} represented by entry has a free-form remote input.
     * Such an input can be used e.g. to implement smart reply buttons - by passing the replies
     * through the remote input.
     */
    public static boolean hasFreeformRemoteInput(NotificationEntry entry) {
        Notification notification = entry.notification.getNotification();
        return null != notification.findRemoteInputActionPair(true /* freeform */);
    }

    /**
     * A storage for smart replies and smart action.
     */
    public static class SmartRepliesAndActions {
        @Nullable public final SmartReplyView.SmartReplies smartReplies;
        @Nullable public final SmartReplyView.SmartActions smartActions;

        SmartRepliesAndActions(
                @Nullable SmartReplyView.SmartReplies smartReplies,
                @Nullable SmartReplyView.SmartActions smartActions) {
            this.smartReplies = smartReplies;
            this.smartActions = smartActions;
        }
    }
}
+0 −240

File changed.

Preview size limit exceeded, changes collapsed.

+279 −0

File added.

Preview size limit exceeded, changes collapsed.