summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Tony Mak <tonymak@google.com> 2018-06-27 18:12:48 +0100
committer Tony Mak <tonymak@google.com> 2018-07-11 19:17:16 +0000
commit09db2ea92409ebfccffe6dc07cc78f4aff5b8a6d (patch)
treecf3bf99982238661fd248fe5bf432ffaec784a97
parent85f01e682eff7b400ed6996f7482b695249db559 (diff)
Suggest smart actions in ExtServices
By using text textclassifier API, we classify entities like email, phone, address in the notification and suggest the corresponding actions. Test: Manual test for now. Sideload GoogleExtServices. Write a sample app to generate notification with phone number / address, etc, and finally observe the smart actions. BUG: 110527159 Change-Id: I02740cb07fa25a588d9e864990f95332d6830f12
-rw-r--r--core/java/android/app/Notification.java11
-rw-r--r--packages/ExtServices/src/android/ext/services/notification/Assistant.java27
-rw-r--r--packages/ExtServices/src/android/ext/services/notification/SmartActionsHelper.java202
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;
+ }
+}