diff options
3 files changed, 234 insertions, 6 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 05bf6bfa3a8f..590fbc7f6a7c 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -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"; diff --git a/packages/ExtServices/src/android/ext/services/notification/Assistant.java b/packages/ExtServices/src/android/ext/services/notification/Assistant.java index fdd6a9caefa3..a539b1f2ba60 100644 --- a/packages/ExtServices/src/android/ext/services/notification/Assistant.java +++ b/packages/ExtServices/src/android/ext/services/notification/Assistant.java @@ -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,7 +191,26 @@ public class Assistant extends NotificationAssistantService { @Override public Adjustment onNotificationEnqueued(StatusBarNotification sbn) { if (DEBUG) Log.i(TAG, "ENQUEUED " + sbn.getKey()); - return null; + 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 @@ -378,4 +401,4 @@ public class Assistant extends NotificationAssistantService { } } } -}
\ No newline at end of file +} diff --git a/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java new file mode 100644 index 000000000000..1754461b844d --- /dev/null +++ b/packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java @@ -0,0 +1,202 @@ +/** + * 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; + } +} |