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

Commit 0951381f authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Suggest smart actions in ExtServices"

parents ef6a5bf7 09db2ea9
Loading
Loading
Loading
Loading
+7 −4
Original line number Diff line number Diff line
@@ -202,6 +202,11 @@ public class Notification implements Parcelable
     */
    private static final int MAX_REPLY_HISTORY = 5;

    /**
     * Maximum numbers of action buttons in a notification.
     * @hide
     */
    public static final int MAX_ACTION_BUTTONS = 3;

    /**
     * If the notification contained an unsent draft for a RemoteInput when the user clicked on it,
@@ -3151,8 +3156,6 @@ public class Notification implements Parcelable
        public static final String EXTRA_REBUILD_HEADS_UP_CONTENT_VIEW_ACTION_COUNT
                = "android.rebuild.hudViewActionCount";

        private static final int MAX_ACTION_BUTTONS = 3;

        private static final boolean USE_ONLY_TITLE_IN_LOW_PRIORITY_SUMMARY =
                SystemProperties.getBoolean("notifications.only_title", true);

@@ -7162,8 +7165,8 @@ public class Notification implements Parcelable
        }

        public static final class Message {

            static final String KEY_TEXT = "text";
            /** @hide */
            public static final String KEY_TEXT = "text";
            static final String KEY_TIMESTAMP = "time";
            static final String KEY_SENDER = "sender";
            static final String KEY_SENDER_PERSON = "sender_person";
+25 −2
Original line number Diff line number Diff line
@@ -19,7 +19,9 @@ package android.ext.services.notification;
import static android.app.NotificationManager.IMPORTANCE_MIN;
import static android.service.notification.NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE;

import android.annotation.NonNull;
import android.app.INotificationManager;
import android.app.Notification;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
@@ -80,6 +82,7 @@ public class Assistant extends NotificationAssistantService {

    private float mDismissToViewRatioLimit;
    private int mStreakLimit;
    private SmartActionsHelper mSmartActionsHelper;

    // key : impressions tracker
    // TODO: prune deleted channels and apps
@@ -99,6 +102,7 @@ public class Assistant extends NotificationAssistantService {
        // Contexts are correctly hooked up by the creation step, which is required for the observer
        // to be hooked up/initialized.
        new SettingsObserver(mHandler);
        mSmartActionsHelper = new SmartActionsHelper();
    }

    private void loadFile() {
@@ -187,8 +191,27 @@ public class Assistant extends NotificationAssistantService {
    @Override
    public Adjustment onNotificationEnqueued(StatusBarNotification sbn) {
        if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey());
        ArrayList<Notification.Action> actions =
                mSmartActionsHelper.suggestActions(this, sbn);
        if (actions.isEmpty()) {
            return null;
        }
        return createEnqueuedNotificationAdjustment(sbn, actions);
    }

    /** A convenience helper for creating an adjustment for an SBN. */
    private Adjustment createEnqueuedNotificationAdjustment(
            @NonNull StatusBarNotification statusBarNotification,
            @NonNull ArrayList<Notification.Action> smartActions) {
        Bundle signals = new Bundle();
        signals.putParcelableArrayList(Adjustment.KEY_SMART_ACTIONS, smartActions);
        return new Adjustment(
                statusBarNotification.getPackageName(),
                statusBarNotification.getKey(),
                signals,
                "smart action" /* explanation */,
                statusBarNotification.getUserId());
    }

    @Override
    public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) {
+202 −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.
 */
package android.ext.services.notification;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Notification;
import android.app.RemoteAction;
import android.content.Context;
import android.os.Bundle;
import android.os.Parcelable;
import android.service.notification.StatusBarNotification;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;

import com.android.internal.util.Preconditions;

import java.util.ArrayList;
import java.util.Collections;

public class SmartActionsHelper {
    private static final ArrayList<Notification.Action> EMPTY_LIST = new ArrayList<>();

    // If a notification has any of these flags set, it's inelgibile for actions being added.
    private static final int FLAG_MASK_INELGIBILE_FOR_ACTIONS =
            Notification.FLAG_ONGOING_EVENT
                    | Notification.FLAG_FOREGROUND_SERVICE
                    | Notification.FLAG_GROUP_SUMMARY
                    | Notification.FLAG_NO_CLEAR;
    private static final int MAX_ACTION_EXTRACTION_TEXT_LENGTH = 400;
    private static final int MAX_ACTIONS_PER_LINK = 1;
    private static final int MAX_SMART_ACTIONS = Notification.MAX_ACTION_BUTTONS;

    SmartActionsHelper() {}

    /**
     * Adds action adjustments based on the notification contents.
     *
     * TODO: Once we have a API in {@link TextClassificationManager} to predict smart actions
     * from notification text / message, we can replace most of the code here by consuming that API.
     */
    @NonNull
    ArrayList<Notification.Action> suggestActions(
            @Nullable Context context, @NonNull StatusBarNotification sbn) {
        if (!isEligibleForActionAdjustment(sbn)) {
            return EMPTY_LIST;
        }
        if (context == null) {
            return EMPTY_LIST;
        }
        TextClassificationManager tcm = context.getSystemService(TextClassificationManager.class);
        if (tcm == null) {
            return EMPTY_LIST;
        }
        Notification.Action[] actions = sbn.getNotification().actions;
        int numOfExistingActions = actions == null ? 0: actions.length;
        int maxSmartActions = MAX_SMART_ACTIONS - numOfExistingActions;
        return suggestActionsFromText(
                tcm,
                getMostSalientActionText(sbn.getNotification()), maxSmartActions);
    }

    /**
     * Returns whether a notification is eligible for action adjustments.
     *
     * <p>We exclude system notifications, those that get refreshed frequently, or ones that relate
     * to fundamental phone functionality where any error would result in a very negative user
     * experience.
     */
    private boolean isEligibleForActionAdjustment(@NonNull StatusBarNotification sbn) {
        Notification notification = sbn.getNotification();
        String pkg = sbn.getPackageName();
        if (notification.actions != null
                && notification.actions.length >= Notification.MAX_ACTION_BUTTONS) {
            return false;
        }
        if (0 != (notification.flags & FLAG_MASK_INELGIBILE_FOR_ACTIONS)) {
            return false;
        }
        if (TextUtils.isEmpty(pkg) || pkg.equals("android")) {
            return false;
        }
        // For now, we are only interested in messages.
        return Notification.CATEGORY_MESSAGE.equals(notification.category)
                || Notification.MessagingStyle.class.equals(notification.getNotificationStyle());
    }

    /** Returns the text most salient for action extraction in a notification. */
    @Nullable
    private CharSequence getMostSalientActionText(@NonNull Notification notification) {
        /* If it's messaging style, use the most recent message. */
        Parcelable[] messages = notification.extras.getParcelableArray(Notification.EXTRA_MESSAGES);
        if (messages != null && messages.length != 0) {
            Bundle lastMessage = (Bundle) messages[messages.length - 1];
            CharSequence lastMessageText =
                    lastMessage.getCharSequence(Notification.MessagingStyle.Message.KEY_TEXT);
            if (!TextUtils.isEmpty(lastMessageText)) {
                return lastMessageText;
            }
        }

        // Fall back to using the normal text.
        return notification.extras.getCharSequence(Notification.EXTRA_TEXT);
    }

    /** Returns a list of actions to act on entities in a given piece of text. */
    @NonNull
    private ArrayList<Notification.Action> suggestActionsFromText(
            @NonNull TextClassificationManager tcm, @Nullable CharSequence text,
            int maxSmartActions) {
        if (TextUtils.isEmpty(text)) {
            return EMPTY_LIST;
        }
        TextClassifier textClassifier = tcm.getTextClassifier();

        // We want to process only text visible to the user to avoid confusing suggestions, so we
        // truncate the text to a reasonable length. This is particularly important for e.g.
        // email apps that sometimes include the text for the entire thread.
        text = text.subSequence(0, Math.min(text.length(), MAX_ACTION_EXTRACTION_TEXT_LENGTH));

        // Extract all entities.
        TextLinks.Request textLinksRequest = new TextLinks.Request.Builder(text)
                .setEntityConfig(
                        TextClassifier.EntityConfig.createWithHints(
                                Collections.singletonList(
                                        TextClassifier.HINT_TEXT_IS_NOT_EDITABLE)))
                .build();
        TextLinks links = textClassifier.generateLinks(textLinksRequest);
        ArrayMap<String, Integer> entityTypeFrequency = getEntityTypeFrequency(links);

        ArrayList<Notification.Action> actions = new ArrayList<>();
        for (TextLinks.TextLink link : links.getLinks()) {
            // Ignore any entity type for which we have too many entities. This is to handle the
            // case where a notification contains e.g. a list of phone numbers. In such cases, the
            // user likely wants to act on the whole list rather than an individual entity.
            if (link.getEntityCount() == 0
                    || entityTypeFrequency.get(link.getEntity(0)) != 1) {
                continue;
            }

            // Generate the actions, and add the most prominent ones to the action bar.
            TextClassification classification =
                    textClassifier.classifyText(
                            new TextClassification.Request.Builder(
                                    text, link.getStart(), link.getEnd()).build());
            int numOfActions = Math.min(
                    MAX_ACTIONS_PER_LINK, classification.getActions().size());
            for (int i = 0; i < numOfActions; ++i) {
                RemoteAction action = classification.getActions().get(i);
                actions.add(
                        new Notification.Action.Builder(
                                action.getIcon(),
                                action.getTitle(),
                                action.getActionIntent())
                                .build());
                // We have enough smart actions.
                if (actions.size() >= maxSmartActions) {
                    return actions;
                }
            }
        }
        return actions;
    }

    /**
     * Given the links extracted from a piece of text, returns the frequency of each entity
     * type.
     */
    @NonNull
    private ArrayMap<String, Integer> getEntityTypeFrequency(@NonNull TextLinks links) {
        ArrayMap<String, Integer> entityTypeCount = new ArrayMap<>();
        for (TextLinks.TextLink link : links.getLinks()) {
            if (link.getEntityCount() == 0) {
                continue;
            }
            String entityType = link.getEntity(0);
            if (entityTypeCount.containsKey(entityType)) {
                entityTypeCount.put(entityType, entityTypeCount.get(entityType) + 1);
            } else {
                entityTypeCount.put(entityType, 1);
            }
        }
        return entityTypeCount;
    }
}