Loading core/java/android/app/Notification.java +7 −4 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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); Loading Loading @@ -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"; Loading packages/ExtServices/src/android/ext/services/notification/Assistant.java +25 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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() { Loading Loading @@ -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) { Loading packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java 0 → 100644 +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; } } Loading
core/java/android/app/Notification.java +7 −4 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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); Loading Loading @@ -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"; Loading
packages/ExtServices/src/android/ext/services/notification/Assistant.java +25 −2 Original line number Diff line number Diff line Loading @@ -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; Loading Loading @@ -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 Loading @@ -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() { Loading Loading @@ -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) { Loading
packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java 0 → 100644 +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; } }