OSDN Git Service

Allow NotificationAssistantService to suggest smart actions
authorTony Mak <tonymak@google.com>
Tue, 19 Jun 2018 17:30:41 +0000 (18:30 +0100)
committerTony Mak <tonymak@google.com>
Wed, 11 Jul 2018 10:12:29 +0000 (11:12 +0100)
Here is the flow:
NAS generates Adjustment -> NMS convert this to RankingUpdate ->
SystemUI.NotificationListener receives the RankingUpdate in either
onNotificationPosted / onNotificationRankingUpdate (Depend on does NAS
provides the adjustment before the notification is en-queued) ->
NotificationEntryManager determines the need of reinflation ->
NotificationInflater inflates / reinflates the view with these
extra bits like smart actions.

Note: We do re-inflation here as simply adding a button to the existing
notification view seems problematic. For example, if the original
notification does not have any action, we will need to inflate the
template with the action container.

Screenshot:
https://hsv.googleplex.com/5731489463402496

Test: atest SystemUITests
Test: atest com.android.server.notification.NotificationAdjustmentExtractorTest
Test: Modify ExtServices to provide adjustment in
      createEnqueuedNotificationAdjustment, post a notification with
      a entity in a sample app, observed the notification is updated.
      (Testing the onNotificationPosted flow)
Test: Modify ExtServices to provide adjustment in onNotificationPosted
      by calling adjustNotification. Post a notification with
      a entity in a sample app, observed the notification is updated.
      (Testing the onRankingUpdated flow)
Test: Repeat the above test, but explicitly make the RowInflaterTask
      slow by inserting Thread.sleep. This can test the onRankingUpdated
      flow when the row is not yet inflated.

BUG: 110527159

Change-Id: I98aee3ac62f60b189ea92ac9fc000127325dfead

18 files changed:
api/system-current.txt
api/test-current.txt
core/java/android/service/notification/Adjustment.java
core/java/android/service/notification/NotificationListenerService.java
core/java/android/service/notification/NotificationRankingUpdate.java
packages/SystemUI/src/com/android/systemui/statusbar/ExpandableNotificationRow.java
packages/SystemUI/src/com/android/systemui/statusbar/NotificationData.java
packages/SystemUI/src/com/android/systemui/statusbar/NotificationEntryManager.java
packages/SystemUI/src/com/android/systemui/statusbar/NotificationUiAdjustment.java [new file with mode: 0644]
packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationInflater.java
packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationDataTest.java
packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationEntryManagerTest.java
packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationUiAdjustmentTest.java [new file with mode: 0644]
services/core/java/com/android/server/notification/NotificationManagerService.java
services/core/java/com/android/server/notification/NotificationRecord.java
services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java
services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java
services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java

index 049a9d2..7c0d958 100644 (file)
@@ -4664,6 +4664,7 @@ package android.service.notification {
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR;
     field public static final java.lang.String KEY_PEOPLE = "key_people";
+    field public static final java.lang.String KEY_SMART_ACTIONS = "key_smart_actions";
     field public static final java.lang.String KEY_SNOOZE_CRITERIA = "key_snooze_criteria";
     field public static final java.lang.String KEY_USER_SENTIMENT = "key_user_sentiment";
   }
index e052578..3b191ac 100644 (file)
@@ -1041,6 +1041,7 @@ package android.service.notification {
     method public void writeToParcel(android.os.Parcel, int);
     field public static final android.os.Parcelable.Creator<android.service.notification.Adjustment> CREATOR;
     field public static final java.lang.String KEY_PEOPLE = "key_people";
+    field public static final java.lang.String KEY_SMART_ACTIONS = "key_smart_actions";
     field public static final java.lang.String KEY_SNOOZE_CRITERIA = "key_snooze_criteria";
     field public static final java.lang.String KEY_USER_SENTIMENT = "key_user_sentiment";
   }
index 7348cf6..0d94af4 100644 (file)
@@ -65,6 +65,12 @@ public final class Adjustment implements Parcelable {
     public static final String KEY_USER_SENTIMENT = "key_user_sentiment";
 
     /**
+     * Data type: ArrayList of {@link android.app.Notification.Action}.
+     * Used to suggest extra actions for a notification.
+     */
+    public static final String KEY_SMART_ACTIONS = "key_smart_actions";
+
+    /**
      * Create a notification adjustment.
      *
      * @param pkg The package of the notification.
index a7d70d0..09425a9 100644 (file)
@@ -1426,6 +1426,7 @@ public abstract class NotificationListenerService extends Service {
         private boolean mShowBadge;
         private @UserSentiment int mUserSentiment = USER_SENTIMENT_NEUTRAL;
         private boolean mHidden;
+        private ArrayList<Notification.Action> mSmartActions;
 
         public Ranking() {}
 
@@ -1556,6 +1557,13 @@ public abstract class NotificationListenerService extends Service {
         }
 
         /**
+         * @hide
+         */
+        public List<Notification.Action> getSmartActions() {
+            return mSmartActions;
+        }
+
+        /**
          * Returns whether this notification can be displayed as a badge.
          *
          * @return true if the notification can be displayed as a badge, false otherwise.
@@ -1583,7 +1591,7 @@ public abstract class NotificationListenerService extends Service {
                 CharSequence explanation, String overrideGroupKey,
                 NotificationChannel channel, ArrayList<String> overridePeople,
                 ArrayList<SnoozeCriterion> snoozeCriteria, boolean showBadge,
-                int userSentiment, boolean hidden) {
+                int userSentiment, boolean hidden, ArrayList<Notification.Action> smartActions) {
             mKey = key;
             mRank = rank;
             mIsAmbient = importance < NotificationManager.IMPORTANCE_LOW;
@@ -1599,6 +1607,7 @@ public abstract class NotificationListenerService extends Service {
             mShowBadge = showBadge;
             mUserSentiment = userSentiment;
             mHidden = hidden;
+            mSmartActions = smartActions;
         }
 
         /**
@@ -1648,6 +1657,7 @@ public abstract class NotificationListenerService extends Service {
         private ArrayMap<String, Boolean> mShowBadge;
         private ArrayMap<String, Integer> mUserSentiment;
         private ArrayMap<String, Boolean> mHidden;
+        private ArrayMap<String, ArrayList<Notification.Action>> mSmartActions;
 
         private RankingMap(NotificationRankingUpdate rankingUpdate) {
             mRankingUpdate = rankingUpdate;
@@ -1676,7 +1686,7 @@ public abstract class NotificationListenerService extends Service {
                     getVisibilityOverride(key), getSuppressedVisualEffects(key),
                     getImportance(key), getImportanceExplanation(key), getOverrideGroupKey(key),
                     getChannel(key), getOverridePeople(key), getSnoozeCriteria(key),
-                    getShowBadge(key), getUserSentiment(key), getHidden(key));
+                    getShowBadge(key), getUserSentiment(key), getHidden(key), getSmartActions(key));
             return rank >= 0;
         }
 
@@ -1814,6 +1824,15 @@ public abstract class NotificationListenerService extends Service {
             return hidden == null ? false : hidden.booleanValue();
         }
 
+        private ArrayList<Notification.Action> getSmartActions(String key) {
+            synchronized (this) {
+                if (mSmartActions == null) {
+                    buildSmartActions();
+                }
+            }
+            return mSmartActions.get(key);
+        }
+
         // Locked by 'this'
         private void buildRanksLocked() {
             String[] orderedKeys = mRankingUpdate.getOrderedKeys();
@@ -1931,6 +1950,15 @@ public abstract class NotificationListenerService extends Service {
             }
         }
 
+        // Locked by 'this'
+        private void buildSmartActions() {
+            Bundle smartActions = mRankingUpdate.getSmartActions();
+            mSmartActions = new ArrayMap<>(smartActions.size());
+            for (String key : smartActions.keySet()) {
+                mSmartActions.put(key, smartActions.getParcelableArrayList(key));
+            }
+        }
+
         // ----------- Parcelable
 
         @Override
index 00c47ec..bed2214 100644 (file)
@@ -37,12 +37,13 @@ public class NotificationRankingUpdate implements Parcelable {
     private final Bundle mShowBadge;
     private final Bundle mUserSentiment;
     private final Bundle mHidden;
+    private final Bundle mSmartActions;
 
     public NotificationRankingUpdate(String[] keys, String[] interceptedKeys,
             Bundle visibilityOverrides, Bundle suppressedVisualEffects,
             int[] importance, Bundle explanation, Bundle overrideGroupKeys,
             Bundle channels, Bundle overridePeople, Bundle snoozeCriteria,
-            Bundle showBadge, Bundle userSentiment, Bundle hidden) {
+            Bundle showBadge, Bundle userSentiment, Bundle hidden, Bundle smartActions) {
         mKeys = keys;
         mInterceptedKeys = interceptedKeys;
         mVisibilityOverrides = visibilityOverrides;
@@ -56,6 +57,7 @@ public class NotificationRankingUpdate implements Parcelable {
         mShowBadge = showBadge;
         mUserSentiment = userSentiment;
         mHidden = hidden;
+        mSmartActions = smartActions;
     }
 
     public NotificationRankingUpdate(Parcel in) {
@@ -73,6 +75,7 @@ public class NotificationRankingUpdate implements Parcelable {
         mShowBadge = in.readBundle();
         mUserSentiment = in.readBundle();
         mHidden = in.readBundle();
+        mSmartActions = in.readBundle();
     }
 
     @Override
@@ -95,6 +98,7 @@ public class NotificationRankingUpdate implements Parcelable {
         out.writeBundle(mShowBadge);
         out.writeBundle(mUserSentiment);
         out.writeBundle(mHidden);
+        out.writeBundle(mSmartActions);
     }
 
     public static final Parcelable.Creator<NotificationRankingUpdate> CREATOR
@@ -159,4 +163,8 @@ public class NotificationRankingUpdate implements Parcelable {
     public Bundle getHidden() {
         return mHidden;
     }
+
+    public Bundle getSmartActions() {
+        return mSmartActions;
+    }
 }
index 9393d5b..cdd9246 100644 (file)
@@ -24,6 +24,7 @@ import android.animation.AnimatorListenerAdapter;
 import android.animation.ObjectAnimator;
 import android.animation.ValueAnimator.AnimatorUpdateListener;
 import android.annotation.Nullable;
+import android.app.Notification;
 import android.app.NotificationChannel;
 import android.content.Context;
 import android.content.pm.PackageInfo;
@@ -1448,6 +1449,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
         mNotificationInflater.setUsesIncreasedHeight(use);
     }
 
+    public void setSmartActions(List<Notification.Action> smartActions) {
+        mNotificationInflater.setSmartActions(smartActions);
+    }
+
     public void setUseIncreasedHeadsUpHeight(boolean use) {
         mUseIncreasedHeadsUpHeight = use;
         mNotificationInflater.setUsesIncreasedHeadsUpHeight(use);
index a58752c..93433da 100644 (file)
@@ -28,6 +28,7 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
 
 import android.Manifest;
+import android.annotation.NonNull;
 import android.app.AppGlobals;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -51,6 +52,8 @@ import android.view.View;
 import android.widget.ImageView;
 import android.widget.RemoteViews;
 
+import androidx.annotation.Nullable;
+
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.statusbar.StatusBarIcon;
 import com.android.internal.util.ArrayUtils;
@@ -105,6 +108,8 @@ public class NotificationData {
         public CharSequence remoteInputText;
         public List<SnoozeCriterion> snoozeCriteria;
         public int userSentiment = Ranking.USER_SENTIMENT_NEUTRAL;
+        @NonNull
+        public List<Notification.Action> smartActions = Collections.emptyList();
 
         private int mCachedContrastColor = COLOR_INVALID;
         private int mCachedContrastColorIsFor = COLOR_INVALID;
@@ -131,8 +136,23 @@ public class NotificationData {
         private boolean hasSentReply;
 
         public Entry(StatusBarNotification n) {
+            this(n, null);
+        }
+
+        public Entry(StatusBarNotification n, @Nullable Ranking ranking) {
             this.key = n.getKey();
             this.notification = n;
+            if (ranking != null) {
+                populateFromRanking(ranking);
+            }
+        }
+
+        public void populateFromRanking(@NonNull Ranking ranking) {
+            channel = ranking.getChannel();
+            snoozeCriteria = ranking.getSnoozeCriteria();
+            userSentiment = ranking.getUserSentiment();
+            smartActions = ranking.getSmartActions() == null
+                    ? Collections.emptyList() : ranking.getSmartActions();
         }
 
         public void setInterruption() {
@@ -232,6 +252,7 @@ public class NotificationData {
 
         /**
          * Update the notification icons.
+         *
          * @param context the context to create the icons with.
          * @param sbn the notification to read the icon from.
          * @throws InflationException
@@ -291,7 +312,7 @@ public class NotificationData {
         }
 
         public void onInflationTaskFinished() {
-           mRunningTask = null;
+            mRunningTask = null;
         }
 
         @VisibleForTesting
@@ -607,7 +628,7 @@ public class NotificationData {
             getRanking(key, mTmpRanking);
             return mTmpRanking.getOverrideGroupKey();
         }
-         return null;
+        return null;
     }
 
     public List<SnoozeCriterion> getSnoozeCriteria(String key) {
@@ -658,9 +679,7 @@ public class NotificationData {
                         entry.notification.setOverrideGroupKey(overrideGroupKey);
                         mGroupManager.onEntryUpdated(entry, oldSbn);
                     }
-                    entry.channel = getChannel(entry.key);
-                    entry.snoozeCriteria = getSnoozeCriteria(entry.key);
-                    entry.userSentiment = mTmpRanking.getUserSentiment();
+                    entry.populateFromRanking(mTmpRanking);
                 }
             }
         }
@@ -833,6 +852,7 @@ public class NotificationData {
         public boolean isNotificationForCurrentProfiles(StatusBarNotification sbn);
         public String getCurrentMediaNotificationKey();
         public NotificationGroupManager getGroupManager();
+
         /**
          * @return true iff the device is dozing
          */
index 06f26c9..bf07929 100644 (file)
@@ -19,6 +19,7 @@ import static com.android.systemui.statusbar.NotificationRemoteInputManager.ENAB
 import static com.android.systemui.statusbar.NotificationRemoteInputManager
         .FORCE_REMOTE_INPUT_HISTORY;
 
+import android.annotation.Nullable;
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
@@ -37,6 +38,7 @@ import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationStats;
 import android.service.notification.StatusBarNotification;
 import android.text.TextUtils;
+import android.util.ArrayMap;
 import android.util.ArraySet;
 import android.util.EventLog;
 import android.util.Log;
@@ -726,10 +728,10 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
                 && !mPresenter.isPresenterFullyCollapsed();
         row.setUseIncreasedCollapsedHeight(useIncreasedCollapsedHeight);
         row.setUseIncreasedHeadsUpHeight(useIncreasedHeadsUp);
+        row.setSmartActions(entry.smartActions);
         row.updateNotification(entry);
     }
 
-
     protected void addNotificationViews(NotificationData.Entry entry) {
         if (entry == null) {
             return;
@@ -740,12 +742,13 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
         updateNotifications();
     }
 
-    protected NotificationData.Entry createNotificationViews(StatusBarNotification sbn)
+    protected NotificationData.Entry createNotificationViews(
+            StatusBarNotification sbn, NotificationListenerService.Ranking ranking)
             throws InflationException {
         if (DEBUG) {
-            Log.d(TAG, "createNotificationViews(notification=" + sbn);
+            Log.d(TAG, "createNotificationViews(notification=" + sbn + " " + ranking);
         }
-        NotificationData.Entry entry = new NotificationData.Entry(sbn);
+        NotificationData.Entry entry = new NotificationData.Entry(sbn, ranking);
         Dependency.get(LeakDetector.class).trackInstance(entry);
         entry.createIcons(mContext, sbn);
         // Construct the expanded view.
@@ -754,12 +757,14 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
     }
 
     private void addNotificationInternal(StatusBarNotification notification,
-            NotificationListenerService.RankingMap ranking) throws InflationException {
+            NotificationListenerService.RankingMap rankingMap) throws InflationException {
         String key = notification.getKey();
         if (DEBUG) Log.d(TAG, "addNotification key=" + key);
 
-        mNotificationData.updateRanking(ranking);
-        NotificationData.Entry shadeEntry = createNotificationViews(notification);
+        mNotificationData.updateRanking(rankingMap);
+        NotificationListenerService.Ranking ranking = new NotificationListenerService.Ranking();
+        rankingMap.getRanking(key, ranking);
+        NotificationData.Entry shadeEntry = createNotificationViews(notification, ranking);
         boolean isHeadsUped = shouldPeek(shadeEntry);
         if (!isHeadsUped && notification.getNotification().fullScreenIntent != null) {
             if (shouldSuppressFullScreenIntent(shadeEntry)) {
@@ -905,11 +910,57 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
         mPresenter.updateNotificationViews();
     }
 
-    public void updateNotificationRanking(NotificationListenerService.RankingMap ranking) {
-        mNotificationData.updateRanking(ranking);
+    public void updateNotificationRanking(NotificationListenerService.RankingMap rankingMap) {
+        List<NotificationData.Entry> entries = new ArrayList<>();
+        entries.addAll(mNotificationData.getActiveNotifications());
+        entries.addAll(mPendingNotifications.values());
+
+        // Has a copy of the current UI adjustments.
+        ArrayMap<String, NotificationUiAdjustment> oldAdjustments = new ArrayMap<>();
+        for (NotificationData.Entry entry : entries) {
+            NotificationUiAdjustment adjustment =
+                    NotificationUiAdjustment.extractFromNotificationEntry(entry);
+            oldAdjustments.put(entry.key, adjustment);
+        }
+
+        // Populate notification entries from the new rankings.
+        mNotificationData.updateRanking(rankingMap);
+        updateRankingOfPendingNotifications(rankingMap);
+
+        // By comparing the old and new UI adjustments, reinflate the view accordingly.
+        for (NotificationData.Entry entry : entries) {
+            NotificationUiAdjustment newAdjustment =
+                    NotificationUiAdjustment.extractFromNotificationEntry(entry);
+
+            if (NotificationUiAdjustment.needReinflate(
+                    oldAdjustments.get(entry.key), newAdjustment)) {
+                if (entry.row != null) {
+                    entry.reset();
+                    PackageManager pmUser = StatusBar.getPackageManagerForUser(mContext,
+                            entry.notification.getUser().getIdentifier());
+                    updateNotification(entry, pmUser, entry.notification, entry.row);
+                } else {
+                    // Once the RowInflaterTask is done, it will pick up the updated entry, so
+                    // no-op here.
+                }
+            }
+        }
+
         updateNotifications();
     }
 
+    private void updateRankingOfPendingNotifications(
+            @Nullable NotificationListenerService.RankingMap rankingMap) {
+        if (rankingMap == null) {
+            return;
+        }
+        NotificationListenerService.Ranking tmpRanking = new NotificationListenerService.Ranking();
+        for (NotificationData.Entry pendingNotification : mPendingNotifications.values()) {
+            rankingMap.getRanking(pendingNotification.key, tmpRanking);
+            pendingNotification.populateFromRanking(tmpRanking);
+        }
+    }
+
     protected boolean shouldPeek(NotificationData.Entry entry) {
         return shouldPeek(entry, entry.notification);
     }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationUiAdjustment.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationUiAdjustment.java
new file mode 100644 (file)
index 0000000..e6bdb26
--- /dev/null
@@ -0,0 +1,151 @@
+/*
+ * Copyright 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 com.android.systemui.statusbar;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.RemoteInput;
+import android.graphics.drawable.Icon;
+import android.text.TextUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * By diffing two entries, determines is view reinflation needed.
+ */
+public class NotificationUiAdjustment {
+
+    public final String key;
+    public final List<Notification.Action> smartActions;
+
+    @VisibleForTesting
+    NotificationUiAdjustment(String key, List<Notification.Action> smartActions) {
+        this.key = key;
+        this.smartActions = smartActions == null
+                ? Collections.emptyList()
+                : new ArrayList<>(smartActions);
+    }
+
+    public static NotificationUiAdjustment extractFromNotificationEntry(
+            NotificationData.Entry entry) {
+        return new NotificationUiAdjustment(entry.key, entry.smartActions);
+    }
+
+    public static boolean needReinflate(
+            @NonNull NotificationUiAdjustment oldAdjustment,
+            @NonNull NotificationUiAdjustment newAdjustment) {
+        if (oldAdjustment == newAdjustment) {
+            return false;
+        }
+        return areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions);
+    }
+
+    public static boolean areDifferent(
+            @NonNull List<Notification.Action> first, @NonNull List<Notification.Action> second) {
+        if (first == second) {
+            return false;
+        }
+        if (first == null || second == null) {
+            return true;
+        }
+        if (first.size() != second.size()) {
+            return true;
+        }
+        for (int i = 0; i < first.size(); i++) {
+            Notification.Action firstAction = first.get(i);
+            Notification.Action secondAction = second.get(i);
+
+            if (!TextUtils.equals(firstAction.title, secondAction.title)) {
+                return true;
+            }
+
+            if (areDifferent(firstAction.getIcon(), secondAction.getIcon())) {
+                return true;
+            }
+
+            if (!Objects.equals(firstAction.actionIntent, secondAction.actionIntent)) {
+                return true;
+            }
+
+            if (areDifferent(firstAction.getRemoteInputs(), secondAction.getRemoteInputs())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean areDifferent(@Nullable Icon first, @Nullable Icon second) {
+        if (first == second) {
+            return false;
+        }
+        if (first == null || second == null) {
+            return true;
+        }
+        return !first.sameAs(second);
+    }
+
+    private static boolean areDifferent(
+            @Nullable RemoteInput[] first, @Nullable RemoteInput[] second) {
+        if (first == second) {
+            return false;
+        }
+        if (first == null || second == null) {
+            return true;
+        }
+        if (first.length != second.length) {
+            return true;
+        }
+        for (int i = 0; i < first.length; i++) {
+            RemoteInput firstRemoteInput = first[i];
+            RemoteInput secondRemoteInput = second[i];
+
+            if (!TextUtils.equals(firstRemoteInput.getLabel(), secondRemoteInput.getLabel())) {
+                return true;
+            }
+            if (areDifferent(firstRemoteInput.getChoices(), secondRemoteInput.getChoices())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static boolean areDifferent(
+            @Nullable CharSequence[] first, @Nullable CharSequence[] second) {
+        if (first == second) {
+            return false;
+        }
+        if (first == null || second == null) {
+            return true;
+        }
+        if (first.length != second.length) {
+            return true;
+        }
+        for (int i = 0; i < first.length; i++) {
+            CharSequence firstCharSequence = first[i];
+            CharSequence secondCharSequence = second[i];
+            if (!TextUtils.equals(firstCharSequence, secondCharSequence)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
index 1303057..9d5a682 100644 (file)
@@ -27,15 +27,17 @@ import android.view.View;
 import android.widget.RemoteViews;
 
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.R;
-import com.android.systemui.statusbar.InflationTask;
 import com.android.systemui.statusbar.ExpandableNotificationRow;
+import com.android.systemui.statusbar.InflationTask;
 import com.android.systemui.statusbar.NotificationContentView;
 import com.android.systemui.statusbar.NotificationData;
 import com.android.systemui.statusbar.phone.StatusBar;
 import com.android.systemui.util.Assert;
 
+import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.concurrent.Executor;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadFactory;
@@ -67,6 +69,7 @@ public class NotificationInflater {
     private boolean mIsChildInGroup;
     private InflationCallback mCallback;
     private boolean mRedactAmbient;
+    private List<Notification.Action> mSmartActions;
 
     public NotificationInflater(ExpandableNotificationRow row) {
         mRow = row;
@@ -95,6 +98,10 @@ public class NotificationInflater {
         mUsesIncreasedHeight = usesIncreasedHeight;
     }
 
+    public void setSmartActions(List<Notification.Action> smartActions) {
+        mSmartActions = smartActions;
+    }
+
     public void setUsesIncreasedHeadsUpHeight(boolean usesIncreasedHeight) {
         mUsesIncreasedHeadsUpHeight = usesIncreasedHeight;
     }
@@ -140,7 +147,7 @@ public class NotificationInflater {
         AsyncInflationTask task = new AsyncInflationTask(sbn, reInflateFlags, mRow,
                 mIsLowPriority,
                 mIsChildInGroup, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, mRedactAmbient,
-                mCallback, mRemoteViewClickHandler);
+                mCallback, mRemoteViewClickHandler, mSmartActions);
         if (mCallback != null && mCallback.doInflateSynchronous()) {
             task.onPostExecute(task.doInBackground());
         } else {
@@ -586,13 +593,15 @@ public class NotificationInflater {
         private Exception mError;
         private RemoteViews.OnClickHandler mRemoteViewClickHandler;
         private CancellationSignal mCancellationSignal;
+        private List<Notification.Action> mSmartActions;
 
         private AsyncInflationTask(StatusBarNotification notification,
                 int reInflateFlags, ExpandableNotificationRow row, boolean isLowPriority,
                 boolean isChildInGroup, boolean usesIncreasedHeight,
                 boolean usesIncreasedHeadsUpHeight, boolean redactAmbient,
                 InflationCallback callback,
-                RemoteViews.OnClickHandler remoteViewClickHandler) {
+                RemoteViews.OnClickHandler remoteViewClickHandler,
+                List<Notification.Action> smartActions) {
             mRow = row;
             mSbn = notification;
             mReInflateFlags = reInflateFlags;
@@ -604,6 +613,9 @@ public class NotificationInflater {
             mRedactAmbient = redactAmbient;
             mRemoteViewClickHandler = remoteViewClickHandler;
             mCallback = callback;
+            mSmartActions = smartActions == null
+                    ? Collections.emptyList()
+                    : new ArrayList<>(smartActions);
             NotificationData.Entry entry = row.getEntry();
             entry.setInflationTask(this);
         }
@@ -619,6 +631,9 @@ public class NotificationInflater {
                 final Notification.Builder recoveredBuilder
                         = Notification.Builder.recoverBuilder(mContext,
                         mSbn.getNotification());
+
+                applyChanges(recoveredBuilder);
+
                 Context packageContext = mSbn.getPackageContext(mContext);
                 Notification notification = mSbn.getNotification();
                 if (notification.isMediaNotification()) {
@@ -646,6 +661,18 @@ public class NotificationInflater {
             }
         }
 
+        /**
+         * Apply changes to the given notification builder, like adding smart actions suggested by
+         * a {@link android.service.notification.NotificationAssistantService}.
+         */
+        private void applyChanges(Notification.Builder builder) {
+            if (mSmartActions != null) {
+                for (Notification.Action smartAction : mSmartActions) {
+                    builder.addAction(smartAction);
+                }
+            }
+        }
+
         private void handleError(Exception e) {
             mRow.getEntry().onInflationTaskFinished();
             StatusBarNotification sbn = mRow.getStatusBarNotification();
index 77522e4..f2f5893 100644 (file)
@@ -38,11 +38,16 @@ import static org.mockito.Mockito.when;
 import android.Manifest;
 import android.app.Notification;
 import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Intent;
 import android.content.pm.IPackageManager;
 import android.content.pm.PackageManager;
+import android.graphics.drawable.Icon;
 import android.media.session.MediaSession;
 import android.os.Bundle;
 import android.service.notification.NotificationListenerService;
+import android.service.notification.NotificationListenerService.Ranking;
+import android.service.notification.SnoozeCriterion;
 import android.service.notification.StatusBarNotification;
 import android.support.test.annotation.UiThreadTest;
 import android.support.test.filters.SmallTest;
@@ -72,6 +77,8 @@ public class NotificationDataTest extends SysuiTestCase {
     private static final int UID_ALLOW_DURING_SETUP = 456;
     private static final String TEST_HIDDEN_NOTIFICATION_KEY = "testHiddenNotificationKey";
     private static final String TEST_EXEMPT_DND_VISUAL_SUPPRESSION_KEY = "exempt";
+    private static final NotificationChannel NOTIFICATION_CHANNEL =
+            new NotificationChannel("id", "name", NotificationChannel.USER_LOCKED_IMPORTANCE);
 
     private final StatusBarNotification mMockStatusBarNotification =
             mock(StatusBarNotification.class);
@@ -145,11 +152,9 @@ public class NotificationDataTest extends SysuiTestCase {
     @Test
     public void testChannelSetWhenAdded() {
         mNotificationData.add(mRow.getEntry());
-        Assert.assertTrue(mRow.getEntry().channel != null);
+        assertEquals(NOTIFICATION_CHANNEL, mRow.getEntry().channel);
     }
 
-
-
     @Test
     public void testAllRelevantNotisTaggedWithAppOps() throws Exception {
         mNotificationData.add(mRow.getEntry());
@@ -373,6 +378,32 @@ public class NotificationDataTest extends SysuiTestCase {
         assertFalse(mNotificationData.isExemptFromDndVisualSuppression(entry));
     }
 
+    @Test
+    public void testCreateNotificationDataEntry_RankingUpdate() {
+        Ranking ranking = mock(Ranking.class);
+
+        ArrayList<Notification.Action> smartActions = new ArrayList<>();
+        smartActions.add(createAction());
+        when(ranking.getSmartActions()).thenReturn(smartActions);
+
+        when(ranking.getChannel()).thenReturn(NOTIFICATION_CHANNEL);
+
+        when(ranking.getUserSentiment()).thenReturn(Ranking.USER_SENTIMENT_NEGATIVE);
+
+        SnoozeCriterion snoozeCriterion = new SnoozeCriterion("id", "explanation", "confirmation");
+        ArrayList<SnoozeCriterion> snoozeCriterions = new ArrayList<>();
+        snoozeCriterions.add(snoozeCriterion);
+        when(ranking.getSnoozeCriteria()).thenReturn(snoozeCriterions);
+
+        NotificationData.Entry entry =
+                new NotificationData.Entry(mMockStatusBarNotification, ranking);
+
+        assertEquals(smartActions, entry.smartActions);
+        assertEquals(NOTIFICATION_CHANNEL, entry.channel);
+        assertEquals(Ranking.USER_SENTIMENT_NEGATIVE, entry.userSentiment);
+        assertEquals(snoozeCriterions, entry.snoozeCriteria);
+    }
+
     private void initStatusBarNotification(boolean allowDuringSetup) {
         Bundle bundle = new Bundle();
         bundle.putBoolean(Notification.EXTRA_ALLOW_DURING_SETUP, allowDuringSetup);
@@ -388,12 +419,7 @@ public class NotificationDataTest extends SysuiTestCase {
         }
 
         @Override
-        public NotificationChannel getChannel(String key) {
-            return new NotificationChannel(null, null, 0);
-        }
-
-        @Override
-        protected boolean getRanking(String key, NotificationListenerService.Ranking outRanking) {
+        protected boolean getRanking(String key, Ranking outRanking) {
             super.getRanking(key, outRanking);
             if (key.equals(TEST_HIDDEN_NOTIFICATION_KEY)) {
                 outRanking.populate(key, outRanking.getRank(),
@@ -401,23 +427,31 @@ public class NotificationDataTest extends SysuiTestCase {
                         outRanking.getVisibilityOverride(), outRanking.getSuppressedVisualEffects(),
                         outRanking.getImportance(), outRanking.getImportanceExplanation(),
                         outRanking.getOverrideGroupKey(), outRanking.getChannel(), null, null,
-                        outRanking.canShowBadge(), outRanking.getUserSentiment(), true);
+                        outRanking.canShowBadge(), outRanking.getUserSentiment(), true,
+                        null);
             } else if (key.equals(TEST_EXEMPT_DND_VISUAL_SUPPRESSION_KEY)) {
                 outRanking.populate(key, outRanking.getRank(),
                         outRanking.matchesInterruptionFilter(),
                         outRanking.getVisibilityOverride(), 255,
                         outRanking.getImportance(), outRanking.getImportanceExplanation(),
                         outRanking.getOverrideGroupKey(), outRanking.getChannel(), null, null,
-                        outRanking.canShowBadge(), outRanking.getUserSentiment(), true);
+                        outRanking.canShowBadge(), outRanking.getUserSentiment(), true, null);
             } else {
                 outRanking.populate(key, outRanking.getRank(),
                         outRanking.matchesInterruptionFilter(),
                         outRanking.getVisibilityOverride(), outRanking.getSuppressedVisualEffects(),
                         outRanking.getImportance(), outRanking.getImportanceExplanation(),
-                        outRanking.getOverrideGroupKey(), outRanking.getChannel(), null, null,
-                        outRanking.canShowBadge(), outRanking.getUserSentiment(), false);
+                        outRanking.getOverrideGroupKey(), NOTIFICATION_CHANNEL, null, null,
+                        outRanking.canShowBadge(), outRanking.getUserSentiment(), false, null);
             }
             return true;
         }
     }
+
+    private Notification.Action createAction() {
+        return new Notification.Action.Builder(
+                Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon),
+                "action",
+                PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build();
+    }
 }
index afe16cf..e75e578 100644 (file)
@@ -37,7 +37,10 @@ import android.app.ActivityManager;
 import android.app.AppOpsManager;
 import android.app.Notification;
 import android.app.NotificationManager;
+import android.app.PendingIntent;
 import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
 import android.os.Handler;
 import android.os.Looper;
 import android.os.UserHandle;
@@ -54,6 +57,8 @@ import com.android.internal.statusbar.IStatusBarService;
 import com.android.systemui.ForegroundServiceController;
 import com.android.systemui.R;
 import com.android.systemui.SysuiTestCase;
+import com.android.systemui.statusbar.notification.NotificationInflater;
+import com.android.systemui.statusbar.notification.RowInflaterTask;
 import com.android.systemui.statusbar.notification.VisualStabilityManager;
 import com.android.systemui.statusbar.phone.NotificationGroupManager;
 import com.android.systemui.statusbar.policy.DeviceProvisionedController;
@@ -68,6 +73,9 @@ import org.mockito.ArgumentCaptor;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
 import java.util.concurrent.CountDownLatch;
 import java.util.concurrent.TimeUnit;
 
@@ -99,6 +107,7 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
     @Mock private VisualStabilityManager mVisualStabilityManager;
     @Mock private MetricsLogger mMetricsLogger;
     @Mock private SmartReplyController mSmartReplyController;
+    @Mock private RowInflaterTask mAsyncInflationTask;
 
     private NotificationData.Entry mEntry;
     private StatusBarNotification mSbn;
@@ -139,7 +148,26 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
                     0,
                     NotificationManager.IMPORTANCE_DEFAULT,
                     null, null,
-                    null, null, null, true, sentiment, false);
+                    null, null, null, true, sentiment, false, null);
+            return true;
+        }).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class));
+    }
+
+    private void setSmartActions(String key, ArrayList<Notification.Action> smartActions) {
+        doAnswer(invocationOnMock -> {
+            NotificationListenerService.Ranking ranking = (NotificationListenerService.Ranking)
+                    invocationOnMock.getArguments()[1];
+            ranking.populate(
+                    key,
+                    0,
+                    false,
+                    0,
+                    0,
+                    NotificationManager.IMPORTANCE_DEFAULT,
+                    null, null,
+                    null, null, null, true,
+                    NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL, false,
+                    smartActions);
             return true;
         }).when(mRankingMap).getRanking(eq(key), any(NotificationListenerService.Ranking.class));
     }
@@ -427,4 +455,71 @@ public class NotificationEntryManagerTest extends SysuiTestCase {
         Assert.assertTrue(newSbn.getNotification().extras
                 .getBoolean(Notification.EXTRA_HIDE_SMART_REPLIES, false));
     }
+
+    @Test
+    public void testUpdateNotificationRanking() {
+        when(mPresenter.isDeviceProvisioned()).thenReturn(true);
+        when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true);
+
+        mEntry.row = mRow;
+        mEntry.setInflationTask(mAsyncInflationTask);
+        mEntryManager.getNotificationData().add(mEntry);
+        setSmartActions(mEntry.key, new ArrayList<>(Arrays.asList(createAction())));
+
+        mEntryManager.updateNotificationRanking(mRankingMap);
+        verify(mRow).updateNotification(eq(mEntry));
+        assertEquals(1, mEntry.smartActions.size());
+        assertEquals("action", mEntry.smartActions.get(0).title);
+    }
+
+    @Test
+    public void testUpdateNotificationRanking_noChange() {
+        when(mPresenter.isDeviceProvisioned()).thenReturn(true);
+        when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true);
+
+        mEntry.row = mRow;
+        mEntryManager.getNotificationData().add(mEntry);
+        setSmartActions(mEntry.key, null);
+
+        mEntryManager.updateNotificationRanking(mRankingMap);
+        verify(mRow, never()).updateNotification(eq(mEntry));
+        assertEquals(0, mEntry.smartActions.size());
+    }
+
+    @Test
+    public void testUpdateNotificationRanking_rowNotInflatedYet() {
+        when(mPresenter.isDeviceProvisioned()).thenReturn(true);
+        when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true);
+
+        mEntry.row = null;
+        mEntryManager.getNotificationData().add(mEntry);
+        setSmartActions(mEntry.key, new ArrayList<>(Arrays.asList(createAction())));
+
+        mEntryManager.updateNotificationRanking(mRankingMap);
+        verify(mRow, never()).updateNotification(eq(mEntry));
+        assertEquals(1, mEntry.smartActions.size());
+        assertEquals("action", mEntry.smartActions.get(0).title);
+    }
+
+    @Test
+    public void testUpdateNotificationRanking_pendingNotification() {
+        when(mPresenter.isDeviceProvisioned()).thenReturn(true);
+        when(mPresenter.isNotificationForCurrentProfiles(any())).thenReturn(true);
+
+        mEntry.row = null;
+        mEntryManager.mPendingNotifications.put(mEntry.key, mEntry);
+        setSmartActions(mEntry.key, new ArrayList<>(Arrays.asList(createAction())));
+
+        mEntryManager.updateNotificationRanking(mRankingMap);
+        verify(mRow, never()).updateNotification(eq(mEntry));
+        assertEquals(1, mEntry.smartActions.size());
+        assertEquals("action", mEntry.smartActions.get(0).title);
+    }
+
+    private Notification.Action createAction() {
+        return new Notification.Action.Builder(
+                Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon),
+                "action",
+                PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build();
+    }
 }
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationUiAdjustmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationUiAdjustmentTest.java
new file mode 100644 (file)
index 0000000..ce47e60
--- /dev/null
@@ -0,0 +1,180 @@
+/*
+ * Copyright 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 com.android.systemui.statusbar;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
+import android.support.test.filters.SmallTest;
+
+import com.android.internal.R;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Test;
+
+import java.util.Collections;
+
+@SmallTest
+public class NotificationUiAdjustmentTest extends SysuiTestCase {
+
+    @Test
+    public void needReinflate_differentLength() {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        Notification.Action action =
+                createActionBuilder("first", R.drawable.ic_corp_icon, pendingIntent).build();
+        assertThat(NotificationUiAdjustment.needReinflate(
+                new NotificationUiAdjustment("first", Collections.emptyList()),
+                new NotificationUiAdjustment("second", Collections.singletonList(action))))
+                .isTrue();
+    }
+
+    @Test
+    public void needReinflate_differentLabels() {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        Notification.Action firstAction =
+                createActionBuilder("first", R.drawable.ic_corp_icon, pendingIntent).build();
+        Notification.Action secondAction =
+                createActionBuilder("second", R.drawable.ic_corp_icon, pendingIntent).build();
+
+        assertThat(NotificationUiAdjustment.needReinflate(
+                new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+                new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+                .isTrue();
+    }
+
+    @Test
+    public void needReinflate_differentIcons() {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        Notification.Action firstAction =
+                createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent).build();
+        Notification.Action secondAction =
+                createActionBuilder("same", R.drawable.ic_account_circle, pendingIntent)
+                        .build();
+
+        assertThat(NotificationUiAdjustment.needReinflate(
+                new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+                new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+                .isTrue();
+    }
+
+    @Test
+    public void needReinflate_differentPendingIntent() {
+        PendingIntent firstPendingIntent =
+                PendingIntent.getActivity(mContext, 0, new Intent(Intent.ACTION_VIEW), 0);
+        PendingIntent secondPendingIntent =
+                PendingIntent.getActivity(mContext, 0, new Intent(Intent.ACTION_PROCESS_TEXT), 0);
+        Notification.Action firstAction =
+                createActionBuilder("same", R.drawable.ic_corp_icon, firstPendingIntent)
+                        .build();
+        Notification.Action secondAction =
+                createActionBuilder("same", R.drawable.ic_corp_icon, secondPendingIntent)
+                        .build();
+
+        assertThat(NotificationUiAdjustment.needReinflate(
+                new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+                new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+                .isTrue();
+    }
+
+    @Test
+    public void needReinflate_differentChoices() {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+
+        RemoteInput firstRemoteInput =
+                createRemoteInput("same", "same", new CharSequence[] {"first"});
+        RemoteInput secondRemoteInput =
+                createRemoteInput("same", "same", new CharSequence[] {"second"});
+
+        Notification.Action firstAction =
+                createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+                        .addRemoteInput(firstRemoteInput)
+                        .build();
+        Notification.Action secondAction =
+                createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+                        .addRemoteInput(secondRemoteInput)
+                        .build();
+
+        assertThat(NotificationUiAdjustment.needReinflate(
+                new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+                new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+                .isTrue();
+    }
+
+    @Test
+    public void needReinflate_differentRemoteInputLabel() {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+
+        RemoteInput firstRemoteInput =
+                createRemoteInput("same", "first", new CharSequence[] {"same"});
+        RemoteInput secondRemoteInput =
+                createRemoteInput("same", "second", new CharSequence[] {"same"});
+
+        Notification.Action firstAction =
+                createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+                        .addRemoteInput(firstRemoteInput)
+                        .build();
+        Notification.Action secondAction =
+                createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+                        .addRemoteInput(secondRemoteInput)
+                        .build();
+
+        assertThat(NotificationUiAdjustment.needReinflate(
+                new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+                new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+                .isTrue();
+    }
+
+    @Test
+    public void needReinflate_negative() {
+        PendingIntent pendingIntent =
+                PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+        RemoteInput firstRemoteInput =
+                createRemoteInput("same", "same", new CharSequence[] {"same"});
+        RemoteInput secondRemoteInput =
+                createRemoteInput("same", "same", new CharSequence[] {"same"});
+
+        Notification.Action firstAction =
+                createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+                        .addRemoteInput(firstRemoteInput).build();
+        Notification.Action secondAction =
+                createActionBuilder("same", R.drawable.ic_corp_icon, pendingIntent)
+                        .addRemoteInput(secondRemoteInput).build();
+
+        assertThat(NotificationUiAdjustment.needReinflate(
+                new NotificationUiAdjustment("first", Collections.singletonList(firstAction)),
+                new NotificationUiAdjustment("second", Collections.singletonList(secondAction))))
+                .isFalse();
+    }
+
+    private Notification.Action.Builder createActionBuilder(
+            String title, int drawableRes, PendingIntent pendingIntent) {
+        return new Notification.Action.Builder(
+                Icon.createWithResource(mContext, drawableRes), title, pendingIntent);
+    }
+
+    private RemoteInput createRemoteInput(String resultKey, String label, CharSequence[] choices) {
+        return new RemoteInput.Builder(resultKey).setLabel(label).setChoices(choices).build();
+    }
+}
index 52f4461..94e8278 100644 (file)
@@ -5188,6 +5188,7 @@ public class NotificationManagerService extends SystemService {
             ArrayList<ArrayList<SnoozeCriterion>> snoozeCriteriaBefore = new ArrayList<>(N);
             ArrayList<Integer> userSentimentBefore = new ArrayList<>(N);
             ArrayList<Integer> suppressVisuallyBefore = new ArrayList<>(N);
+            ArrayList<ArrayList<Notification.Action>> smartActionsBefore = new ArrayList<>(N);
             for (int i = 0; i < N; i++) {
                 final NotificationRecord r = mNotificationList.get(i);
                 orderBefore.add(r.getKey());
@@ -5199,6 +5200,7 @@ public class NotificationManagerService extends SystemService {
                 snoozeCriteriaBefore.add(r.getSnoozeCriteria());
                 userSentimentBefore.add(r.getUserSentiment());
                 suppressVisuallyBefore.add(r.getSuppressedVisualEffects());
+                smartActionsBefore.add(r.getSmartActions());
                 mRankingHelper.extractSignals(r);
             }
             mRankingHelper.sort(mNotificationList);
@@ -5213,7 +5215,8 @@ public class NotificationManagerService extends SystemService {
                         || !Objects.equals(snoozeCriteriaBefore.get(i), r.getSnoozeCriteria())
                         || !Objects.equals(userSentimentBefore.get(i), r.getUserSentiment())
                         || !Objects.equals(suppressVisuallyBefore.get(i),
-                        r.getSuppressedVisualEffects())) {
+                        r.getSuppressedVisualEffects())
+                        || !Objects.equals(smartActionsBefore.get(i), r.getSmartActions())) {
                     mHandler.scheduleSendRankingUpdate();
                     return;
                 }
@@ -6196,6 +6199,7 @@ public class NotificationManagerService extends SystemService {
         Bundle showBadge = new Bundle();
         Bundle userSentiment = new Bundle();
         Bundle hidden = new Bundle();
+        Bundle smartActions = new Bundle();
         for (int i = 0; i < N; i++) {
             NotificationRecord record = mNotificationList.get(i);
             if (!isVisibleToListener(record.sbn, info)) {
@@ -6223,6 +6227,7 @@ public class NotificationManagerService extends SystemService {
             showBadge.putBoolean(key, record.canShowBadge());
             userSentiment.putInt(key, record.getUserSentiment());
             hidden.putBoolean(key, record.isHidden());
+            smartActions.putParcelableArrayList(key, record.getSmartActions());
         }
         final int M = keys.size();
         String[] keysAr = keys.toArray(new String[M]);
@@ -6233,7 +6238,8 @@ public class NotificationManagerService extends SystemService {
         }
         return new NotificationRankingUpdate(keysAr, interceptedKeysAr, visibilityOverrides,
                 suppressedVisualEffects, importanceAr, explanation, overrideGroupKeys,
-                channels, overridePeople, snoozeCriteria, showBadge, userSentiment, hidden);
+                channels, overridePeople, snoozeCriteria, showBadge, userSentiment, hidden,
+                smartActions);
     }
 
     boolean hasCompanionDevice(ManagedServiceInfo info) {
index 75b9f13..2c182cb 100644 (file)
@@ -159,6 +159,7 @@ public final class NotificationRecord {
     private Light mLight;
     private String mGroupLogTag;
     private String mChannelIdLogTag;
+    private ArrayList<Notification.Action> mSmartActions;
 
     private final List<Adjustment> mAdjustments;
     private final NotificationStats mStats;
@@ -628,6 +629,9 @@ public final class NotificationRecord {
                                 Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEUTRAL));
                     }
                 }
+                if (signals.containsKey(Adjustment.KEY_SMART_ACTIONS)) {
+                    setSmartActions(signals.getParcelableArrayList(Adjustment.KEY_SMART_ACTIONS));
+                }
             }
         }
     }
@@ -1047,6 +1051,14 @@ public final class NotificationRecord {
         mHasSeenSmartReplies = hasSeenSmartReplies;
     }
 
+    public void setSmartActions(ArrayList<Notification.Action> smartActions) {
+        mSmartActions = smartActions;
+    }
+
+    public ArrayList<Notification.Action> getSmartActions() {
+        return mSmartActions;
+    }
+
     /**
      * @return all {@link Uri} that should have permission granted to whoever
      *         will be rendering it. This list has already been vetted to only
index fd674f0..f17a30d 100644 (file)
@@ -25,6 +25,9 @@ import static junit.framework.Assert.assertTrue;
 
 import android.app.Notification;
 import android.app.NotificationChannel;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.UserHandle;
 import android.service.notification.Adjustment;
@@ -54,6 +57,9 @@ public class NotificationAdjustmentExtractorTest extends UiServiceTestCase {
         ArrayList<String> people = new ArrayList<>();
         people.add("you");
         signals.putStringArrayList(Adjustment.KEY_PEOPLE, people);
+        ArrayList<Notification.Action> smartActions = new ArrayList<>();
+        smartActions.add(createAction());
+        signals.putParcelableArrayList(Adjustment.KEY_SMART_ACTIONS, smartActions);
         Adjustment adjustment = new Adjustment("pkg", r.getKey(), signals, "", 0);
         r.addAdjustment(adjustment);
 
@@ -66,6 +72,7 @@ public class NotificationAdjustmentExtractorTest extends UiServiceTestCase {
         assertTrue(r.getGroupKey().contains(GroupHelper.AUTOGROUP_KEY));
         assertEquals(people, r.getPeopleOverride());
         assertEquals(snoozeCriteria, r.getSnoozeCriteria());
+        assertEquals(smartActions, r.getSmartActions());
     }
 
     @Test
@@ -114,4 +121,11 @@ public class NotificationAdjustmentExtractorTest extends UiServiceTestCase {
                 0, n, UserHandle.ALL, null, System.currentTimeMillis());
        return new NotificationRecord(getContext(), sbn, channel);
     }
+
+    private Notification.Action createAction() {
+        return new Notification.Action.Builder(
+                Icon.createWithResource(getContext(), android.R.drawable.sym_def_app_icon),
+                "action",
+                PendingIntent.getBroadcast(getContext(), 0, new Intent("Action"), 0)).build();
+    }
 }
index ef9ba78..742ad65 100644 (file)
@@ -31,11 +31,14 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import android.app.INotificationManager;
+import android.app.Notification;
 import android.app.NotificationChannel;
+import android.app.PendingIntent;
 import android.content.Intent;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.IBinder;
+import android.os.Parcel;
 import android.service.notification.NotificationListenerService;
 import android.service.notification.NotificationListenerService.Ranking;
 import android.service.notification.NotificationRankingUpdate;
@@ -91,6 +94,7 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
             assertEquals(getShowBadge(i), ranking.canShowBadge());
             assertEquals(getUserSentiment(i), ranking.getUserSentiment());
             assertEquals(getHidden(i), ranking.isSuspended());
+            assertActionsEqual(getSmartActions(key, i), ranking.getSmartActions());
         }
     }
 
@@ -107,6 +111,7 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
         int[] importance = new int[mKeys.length];
         Bundle userSentiment = new Bundle();
         Bundle mHidden = new Bundle();
+        Bundle smartActions = new Bundle();
 
         for (int i = 0; i < mKeys.length; i++) {
             String key = mKeys[i];
@@ -124,11 +129,13 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
             showBadge.putBoolean(key, getShowBadge(i));
             userSentiment.putInt(key, getUserSentiment(i));
             mHidden.putBoolean(key, getHidden(i));
+            smartActions.putParcelableArrayList(key, getSmartActions(key, i));
         }
         NotificationRankingUpdate update = new NotificationRankingUpdate(mKeys,
                 interceptedKeys.toArray(new String[0]), visibilityOverrides,
                 suppressedVisualEffects, importance, explanation, overrideGroupKeys,
-                channels, overridePeople, snoozeCriteria, showBadge, userSentiment, mHidden);
+                channels, overridePeople, snoozeCriteria, showBadge, userSentiment, mHidden,
+                smartActions);
         return update;
     }
 
@@ -196,6 +203,29 @@ public class NotificationListenerServiceTest extends UiServiceTestCase {
         return snooze;
     }
 
+    private ArrayList<Notification.Action> getSmartActions(String key, int index) {
+        ArrayList<Notification.Action> actions = new ArrayList<>();
+        for (int i = 0; i < index; i++) {
+            PendingIntent intent = PendingIntent.getBroadcast(
+                    getContext(),
+                    index /*requestCode*/,
+                    new Intent("ACTION_" + key),
+                    0 /*flags*/);
+            actions.add(new Notification.Action.Builder(null /*icon*/, key, intent).build());
+        }
+        return actions;
+    }
+
+    private void assertActionsEqual(
+            List<Notification.Action> expecteds, List<Notification.Action> actuals) {
+        assertEquals(expecteds.size(), actuals.size());
+        for (int i = 0; i < expecteds.size(); i++) {
+            Notification.Action expected = expecteds.get(i);
+            Notification.Action actual = actuals.get(i);
+            assertEquals(expected.title, actual.title);
+        }
+    }
+
     public static class TestListenerService extends NotificationListenerService {
         private final IBinder binder = new LocalBinder();
 
index e286991..bd6416d 100644 (file)
@@ -34,8 +34,6 @@ import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import android.app.ActivityManager;
@@ -48,6 +46,7 @@ import android.content.Context;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.PackageManager;
 import android.graphics.Color;
+import android.graphics.drawable.Icon;
 import android.media.AudioAttributes;
 import android.metrics.LogMaker;
 import android.net.Uri;
@@ -59,8 +58,8 @@ import android.service.notification.Adjustment;
 import android.service.notification.StatusBarNotification;
 import android.support.test.runner.AndroidJUnit4;
 import android.test.suitebuilder.annotation.SmallTest;
-import android.util.Slog;
 
+import com.android.internal.R;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.server.UiServiceTestCase;
 
@@ -70,6 +69,7 @@ import org.junit.runner.RunWith;
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
+import java.util.ArrayList;
 import java.util.Objects;
 
 @SmallTest
@@ -697,4 +697,20 @@ public class NotificationRecordTest extends UiServiceTestCase {
         record.calculateGrantableUris();
         // should not throw
     }
+
+    @Test
+    public void testSmartActions() {
+        StatusBarNotification sbn = getNotification(PKG_O, true /* noisy */,
+                true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
+                false /* lights */, false /* defaultLights */, groupId /* group */);
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+        assertNull(record.getSmartActions());
+
+        ArrayList<Notification.Action> smartActions = new ArrayList<>();
+        smartActions.add(new Notification.Action.Builder(
+                Icon.createWithResource(getContext(), R.drawable.btn_default),
+                "text", null).build());
+        record.setSmartActions(smartActions);
+        assertEquals(smartActions, record.getSmartActions());
+    }
 }