Merge "[Notif] Allow locking importance on notification" into pi-dev
diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl
index 5067e19..5461c0c 100644
--- a/core/java/android/app/INotificationManager.aidl
+++ b/core/java/android/app/INotificationManager.aidl
@@ -54,6 +54,13 @@
     void setShowBadge(String pkg, int uid, boolean showBadge);
     boolean canShowBadge(String pkg, int uid);
     void setNotificationsEnabledForPackage(String pkg, int uid, boolean enabled);
+    /**
+     * Updates the notification's enabled state. Additionally locks importance for all of the
+     * notifications belonging to the app, such that future notifications aren't reconsidered for
+     * blocking helper.
+     */
+    void setNotificationsEnabledWithImportanceLockForPackage(String pkg, int uid, boolean enabled);
+
     boolean areNotificationsEnabledForPackage(String pkg, int uid);
     boolean areNotificationsEnabled(String pkg);
     int getPackageImportance(String pkg);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java
index 81dd9e8..8062064 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationInfo.java
@@ -523,7 +523,7 @@
                 } else {
                     // For notifications with more than one channel, update notification enabled
                     // state. If the importance was lowered, we disable notifications.
-                    mINotificationManager.setNotificationsEnabledForPackage(
+                    mINotificationManager.setNotificationsEnabledWithImportanceLockForPackage(
                             mPackageName, mAppUid, mNewImportance >= mCurrentImportance);
                 }
             } catch (RemoteException e) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInfoTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInfoTest.java
index 3cbe274..b0530c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInfoTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/NotificationInfoTest.java
@@ -421,6 +421,66 @@
     }
 
     @Test
+    public void testHandleCloseControls_setsNotificationsDisabledForMultipleChannelNotifications()
+            throws Exception {
+        mNotificationChannel.setImportance(IMPORTANCE_LOW);
+        mNotificationInfo.bindNotification(mMockPackageManager, mMockINotificationManager,
+                TEST_PACKAGE_NAME, mNotificationChannel /* notificationChannel */,
+                10 /* numUniqueChannelsInRow */, mSbn, null /* checkSaveListener */,
+                null /* onSettingsClick */, null /* onAppSettingsClick */ ,
+                false /* isNonblockable */);
+
+        mNotificationInfo.findViewById(R.id.block).performClick();
+        waitForUndoButton();
+        mNotificationInfo.handleCloseControls(true, false);
+
+        mTestableLooper.processAllMessages();
+        verify(mMockINotificationManager, times(1))
+                .setNotificationsEnabledWithImportanceLockForPackage(
+                        anyString(), eq(TEST_UID), eq(false));
+    }
+
+
+    @Test
+    public void testHandleCloseControls_keepsNotificationsEnabledForMultipleChannelNotifications()
+            throws Exception {
+        mNotificationChannel.setImportance(IMPORTANCE_LOW);
+        mNotificationInfo.bindNotification(mMockPackageManager, mMockINotificationManager,
+                TEST_PACKAGE_NAME, mNotificationChannel /* notificationChannel */,
+                10 /* numUniqueChannelsInRow */, mSbn, null /* checkSaveListener */,
+                null /* onSettingsClick */, null /* onAppSettingsClick */ ,
+                false /* isNonblockable */);
+
+        mNotificationInfo.findViewById(R.id.block).performClick();
+        waitForUndoButton();
+        mNotificationInfo.handleCloseControls(true, false);
+
+        mTestableLooper.processAllMessages();
+        verify(mMockINotificationManager, times(1))
+                .setNotificationsEnabledWithImportanceLockForPackage(
+                        anyString(), eq(TEST_UID), eq(false));
+    }
+
+    @Test
+    public void testCloseControls_blockingHelperSavesImportanceForMultipleChannelNotifications()
+            throws Exception {
+        mNotificationInfo.bindNotification(mMockPackageManager, mMockINotificationManager,
+                TEST_PACKAGE_NAME, mNotificationChannel /* notificationChannel */,
+                10 /* numUniqueChannelsInRow */, mSbn, null /* checkSaveListener */,
+                null /* onSettingsClick */, null /* onAppSettingsClick */ ,
+                false /* isNonblockable */, true /* isForBlockingHelper */,
+                true /* isUserSentimentNegative */);
+
+        mNotificationInfo.findViewById(R.id.keep).performClick();
+
+        verify(mBlockingHelperManager).dismissCurrentBlockingHelper();
+        mTestableLooper.processAllMessages();
+        verify(mMockINotificationManager, times(1))
+                .setNotificationsEnabledWithImportanceLockForPackage(
+                        anyString(), eq(TEST_UID), eq(true));
+    }
+
+    @Test
     public void testCloseControls_blockingHelperDismissedIfShown() throws Exception {
         mNotificationInfo.bindNotification(
                 mMockPackageManager,
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index d59c9de..2c0f469 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2131,6 +2131,26 @@
         }
 
         /**
+         * Updates the enabled state for notifications for the given package (and uid).
+         * Additionally, this method marks the app importance as locked by the user, which means
+         * that notifications from the app will <b>not</b> be considered for showing a
+         * blocking helper.
+         *
+         * @param pkg package that owns the notifications to update
+         * @param uid uid of the app providing notifications
+         * @param enabled whether notifications should be enabled for the app
+         *
+         * @see #setNotificationsEnabledForPackage(String, int, boolean)
+         */
+        @Override
+        public void setNotificationsEnabledWithImportanceLockForPackage(
+                String pkg, int uid, boolean enabled) {
+            setNotificationsEnabledForPackage(pkg, uid, enabled);
+
+            mRankingHelper.setAppImportanceLocked(pkg, uid);
+        }
+
+        /**
          * Use this when you just want to know if notifications are OK for this package.
          */
         @Override
@@ -3616,6 +3636,8 @@
                                 System.currentTimeMillis());
                 summaryRecord = new NotificationRecord(getContext(), summarySbn,
                         notificationRecord.getChannel());
+                summaryRecord.setIsAppImportanceLocked(
+                        notificationRecord.getIsAppImportanceLocked());
                 summaries.put(pkg, summarySbn.getKey());
             }
         }
@@ -4012,6 +4034,7 @@
                 pkg, opPkg, id, tag, notificationUid, callingPid, notification,
                 user, null, System.currentTimeMillis());
         final NotificationRecord r = new NotificationRecord(getContext(), n, channel);
+        r.setIsAppImportanceLocked(mRankingHelper.getIsAppImportanceLocked(pkg, callingUid));
 
         if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0
                 && (channel.getUserLockedFields() & NotificationChannel.USER_LOCKED_IMPORTANCE) == 0
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index 9bd3e52..9745be3 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -151,11 +151,14 @@
     private boolean mIsInterruptive;
     private int mNumberOfSmartRepliesAdded;
     private boolean mHasSeenSmartReplies;
+    /**
+     * Whether this notification (and its channels) should be considered user locked. Used in
+     * conjunction with user sentiment calculation.
+     */
+    private boolean mIsAppImportanceLocked;
 
-    @VisibleForTesting
     public NotificationRecord(Context context, StatusBarNotification sbn,
-            NotificationChannel channel)
-    {
+            NotificationChannel channel) {
         this.sbn = sbn;
         mOriginalFlags = sbn.getNotification().flags;
         mRankingTimeMs = calculateRankingTimeMs(0L);
@@ -503,6 +506,7 @@
         pw.println(prefix + "mImportance="
                 + NotificationListenerService.Ranking.importanceToString(mImportance));
         pw.println(prefix + "mImportanceExplanation=" + mImportanceExplanation);
+        pw.println(prefix + "mIsAppImportanceLocked=" + mIsAppImportanceLocked);
         pw.println(prefix + "mIntercept=" + mIntercept);
         pw.println(prefix + "mHidden==" + mHidden);
         pw.println(prefix + "mGlobalSortKey=" + mGlobalSortKey);
@@ -564,11 +568,11 @@
     public final String toString() {
         return String.format(
                 "NotificationRecord(0x%08x: pkg=%s user=%s id=%d tag=%s importance=%d key=%s" +
-                        ": %s)",
+                        "appImportanceLocked=%s: %s)",
                 System.identityHashCode(this),
                 this.sbn.getPackageName(), this.sbn.getUser(), this.sbn.getId(),
                 this.sbn.getTag(), this.mImportance, this.sbn.getKey(),
-                this.sbn.getNotification());
+                mIsAppImportanceLocked, this.sbn.getNotification());
     }
 
     public void addAdjustment(Adjustment adjustment) {
@@ -600,7 +604,8 @@
                 if (signals.containsKey(Adjustment.KEY_USER_SENTIMENT)) {
                     // Only allow user sentiment update from assistant if user hasn't already
                     // expressed a preference for this channel
-                    if ((getChannel().getUserLockedFields() & USER_LOCKED_IMPORTANCE) == 0) {
+                    if (!mIsAppImportanceLocked
+                            && (getChannel().getUserLockedFields() & USER_LOCKED_IMPORTANCE) == 0) {
                         setUserSentiment(adjustment.getSignals().getInt(
                                 Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEUTRAL));
                     }
@@ -609,6 +614,11 @@
         }
     }
 
+    public void setIsAppImportanceLocked(boolean isAppImportanceLocked) {
+        mIsAppImportanceLocked = isAppImportanceLocked;
+        calculateUserSentiment();
+    }
+
     public void setContactAffinity(float contactAffinity) {
         mContactAffinity = contactAffinity;
         if (mImportance < IMPORTANCE_DEFAULT &&
@@ -870,6 +880,13 @@
         return mChannel;
     }
 
+    /**
+     * @see RankingHelper#getIsAppImportanceLocked(String, int)
+     */
+    public boolean getIsAppImportanceLocked() {
+        return mIsAppImportanceLocked;
+    }
+
     protected void updateNotificationChannel(NotificationChannel channel) {
         if (channel != null) {
             mChannel = channel;
@@ -927,7 +944,8 @@
     }
 
     private void calculateUserSentiment() {
-        if ((getChannel().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0) {
+        if ((getChannel().getUserLockedFields() & USER_LOCKED_IMPORTANCE) != 0
+                || mIsAppImportanceLocked) {
             mUserSentiment = USER_SENTIMENT_POSITIVE;
         }
     }
diff --git a/services/core/java/com/android/server/notification/RankingHelper.java b/services/core/java/com/android/server/notification/RankingHelper.java
index 43d393a..c2ec79b 100644
--- a/services/core/java/com/android/server/notification/RankingHelper.java
+++ b/services/core/java/com/android/server/notification/RankingHelper.java
@@ -24,6 +24,7 @@
 import com.android.internal.util.Preconditions;
 import com.android.internal.util.XmlUtils;
 
+import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.app.Notification;
 import android.app.NotificationChannel;
@@ -36,7 +37,6 @@
 import android.content.pm.PackageManager.NameNotFoundException;
 import android.content.pm.ParceledListSlice;
 import android.content.pm.Signature;
-import android.content.res.Resources;
 import android.metrics.LogMaker;
 import android.os.Build;
 import android.os.UserHandle;
@@ -89,11 +89,25 @@
     private static final String ATT_VISIBILITY = "visibility";
     private static final String ATT_IMPORTANCE = "importance";
     private static final String ATT_SHOW_BADGE = "show_badge";
+    private static final String ATT_APP_USER_LOCKED_FIELDS = "app_user_locked_fields";
 
     private static final int DEFAULT_PRIORITY = Notification.PRIORITY_DEFAULT;
     private static final int DEFAULT_VISIBILITY = NotificationManager.VISIBILITY_NO_OVERRIDE;
     private static final int DEFAULT_IMPORTANCE = NotificationManager.IMPORTANCE_UNSPECIFIED;
     private static final boolean DEFAULT_SHOW_BADGE = true;
+    /**
+     * Default value for what fields are user locked. See {@link LockableAppFields} for all lockable
+     * fields.
+     */
+    private static final int DEFAULT_LOCKED_APP_FIELDS = 0;
+
+    /**
+     * All user-lockable fields for a given application.
+     */
+    @IntDef({LockableAppFields.USER_LOCKED_IMPORTANCE})
+    public @interface LockableAppFields {
+        int USER_LOCKED_IMPORTANCE = 0x00000001;
+    }
 
     private final NotificationSignalExtractor[] mSignalExtractors;
     private final NotificationComparator mPreliminaryComparator;
@@ -218,6 +232,8 @@
                                 parser, ATT_VISIBILITY, DEFAULT_VISIBILITY);
                         r.showBadge = XmlUtils.readBooleanAttribute(
                                 parser, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE);
+                        r.lockedAppFields = XmlUtils.readIntAttribute(parser,
+                                ATT_APP_USER_LOCKED_FIELDS, DEFAULT_LOCKED_APP_FIELDS);
 
                         final int innerDepth = parser.getDepth();
                         while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
@@ -388,10 +404,14 @@
                 if (forBackup && UserHandle.getUserId(r.uid) != UserHandle.USER_SYSTEM) {
                     continue;
                 }
-                final boolean hasNonDefaultSettings = r.importance != DEFAULT_IMPORTANCE
-                        || r.priority != DEFAULT_PRIORITY || r.visibility != DEFAULT_VISIBILITY
-                        || r.showBadge != DEFAULT_SHOW_BADGE || r.channels.size() > 0
-                        || r.groups.size() > 0;
+                final boolean hasNonDefaultSettings =
+                        r.importance != DEFAULT_IMPORTANCE
+                            || r.priority != DEFAULT_PRIORITY
+                            || r.visibility != DEFAULT_VISIBILITY
+                            || r.showBadge != DEFAULT_SHOW_BADGE
+                            || r.lockedAppFields != DEFAULT_LOCKED_APP_FIELDS
+                            || r.channels.size() > 0
+                            || r.groups.size() > 0;
                 if (hasNonDefaultSettings) {
                     out.startTag(null, TAG_PACKAGE);
                     out.attribute(null, ATT_NAME, r.pkg);
@@ -405,6 +425,8 @@
                         out.attribute(null, ATT_VISIBILITY, Integer.toString(r.visibility));
                     }
                     out.attribute(null, ATT_SHOW_BADGE, Boolean.toString(r.showBadge));
+                    out.attribute(null, ATT_APP_USER_LOCKED_FIELDS,
+                            Integer.toString(r.lockedAppFields));
 
                     if (!forBackup) {
                         out.attribute(null, ATT_UID, Integer.toString(r.uid));
@@ -511,6 +533,17 @@
         return getOrCreateRecord(packageName, uid).importance;
     }
 
+
+    /**
+     * Returns whether the importance of the corresponding notification is user-locked and shouldn't
+     * be adjusted by an assistant (via means of a blocking helper, for example). For the channel
+     * locking field, see {@link NotificationChannel#USER_LOCKED_IMPORTANCE}.
+     */
+    public boolean getIsAppImportanceLocked(String packageName, int uid) {
+        int userLockedFields = getOrCreateRecord(packageName, uid).lockedAppFields;
+        return (userLockedFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0;
+    }
+
     @Override
     public boolean canShowBadge(String packageName, int uid) {
         return getOrCreateRecord(packageName, uid).showBadge;
@@ -996,6 +1029,21 @@
                 enabled ? DEFAULT_IMPORTANCE : IMPORTANCE_NONE);
     }
 
+    /**
+     * Sets whether any notifications from the app, represented by the given {@code pkgName} and
+     * {@code uid}, have their importance locked by the user. Locked notifications don't get
+     * considered for sentiment adjustments (and thus never show a blocking helper).
+     */
+    public void setAppImportanceLocked(String packageName, int uid) {
+        Record record = getOrCreateRecord(packageName, uid);
+        if ((record.lockedAppFields & LockableAppFields.USER_LOCKED_IMPORTANCE) != 0) {
+            return;
+        }
+
+        record.lockedAppFields = record.lockedAppFields | LockableAppFields.USER_LOCKED_IMPORTANCE;
+        updateConfig();
+    }
+
     @VisibleForTesting
     void lockFieldsForUpdate(NotificationChannel original, NotificationChannel update) {
         if (original.canBypassDnd() != update.canBypassDnd()) {
@@ -1413,6 +1461,7 @@
         int priority = DEFAULT_PRIORITY;
         int visibility = DEFAULT_VISIBILITY;
         boolean showBadge = DEFAULT_SHOW_BADGE;
+        int lockedAppFields = DEFAULT_LOCKED_APP_FIELDS;
 
         ArrayMap<String, NotificationChannel> channels = new ArrayMap<>();
         Map<String, NotificationChannelGroup> groups = new ConcurrentHashMap<>();
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
index a566327..6303184 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java
@@ -554,6 +554,34 @@
     }
 
     @Test
+    public void testUserSentiment_appImportanceUpdatesSentiment() throws Exception {
+        StatusBarNotification sbn = getNotification(false /*preO */, true /* noisy */,
+                true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
+                false /* lights */, false /* defaultLights */, groupId /* group */);
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+        assertEquals(USER_SENTIMENT_NEUTRAL, record.getUserSentiment());
+
+        record.setIsAppImportanceLocked(true);
+        assertEquals(USER_SENTIMENT_POSITIVE, record.getUserSentiment());
+    }
+
+    @Test
+    public void testUserSentiment_appImportanceBlocksNegativeSentimentUpdate() throws Exception {
+        StatusBarNotification sbn = getNotification(false /*preO */, true /* noisy */,
+                true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
+                false /* lights */, false /* defaultLights */, groupId /* group */);
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+        record.setIsAppImportanceLocked(true);
+
+        Bundle signals = new Bundle();
+        signals.putInt(Adjustment.KEY_USER_SENTIMENT, USER_SENTIMENT_NEGATIVE);
+        record.addAdjustment(new Adjustment(pkg, record.getKey(), signals, null, sbn.getUserId()));
+        record.applyAdjustments();
+
+        assertEquals(USER_SENTIMENT_POSITIVE, record.getUserSentiment());
+    }
+
+    @Test
     public void testUserSentiment_userLocked() throws Exception {
         channel.lockFields(USER_LOCKED_IMPORTANCE);
         StatusBarNotification sbn = getNotification(false /*preO */, true /* noisy */,
@@ -571,4 +599,18 @@
 
         assertEquals(USER_SENTIMENT_POSITIVE, record.getUserSentiment());
     }
+
+    @Test
+    public void testAppImportance_returnsCorrectly() throws Exception {
+        StatusBarNotification sbn = getNotification(false /*preO */, true /* noisy */,
+                true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */,
+                false /* lights */, false /* defaultLights */, groupId /* group */);
+        NotificationRecord record = new NotificationRecord(mMockContext, sbn, channel);
+
+        record.setIsAppImportanceLocked(true);
+        assertEquals(true, record.getIsAppImportanceLocked());
+
+        record.setIsAppImportanceLocked(false);
+        assertEquals(false, record.getIsAppImportanceLocked());
+    }
 }
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
index 54ed1e6..78aa965 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java
@@ -371,6 +371,7 @@
         mHelper.createNotificationChannel(PKG, UID, channel2, false, false);
 
         mHelper.setShowBadge(PKG, UID, true);
+        mHelper.setAppImportanceLocked(PKG, UID);
 
         ByteArrayOutputStream baos = writeXmlAndPurge(PKG, UID, false, channel1.getId(),
                 channel2.getId(), NotificationChannel.DEFAULT_CHANNEL_ID);
@@ -379,6 +380,7 @@
         loadStreamXml(baos, false);
 
         assertTrue(mHelper.canShowBadge(PKG, UID));
+        assertTrue(mHelper.getIsAppImportanceLocked(PKG, UID));
         assertEquals(channel1, mHelper.getNotificationChannel(PKG, UID, channel1.getId(), false));
         compareChannels(channel2,
                 mHelper.getNotificationChannel(PKG, UID, channel2.getId(), false));
@@ -805,6 +807,7 @@
         assertEquals(Notification.PRIORITY_DEFAULT, mHelper.getPackagePriority(PKG, UID));
         assertEquals(NotificationManager.VISIBILITY_NO_OVERRIDE,
                 mHelper.getPackageVisibility(PKG, UID));
+        assertFalse(mHelper.getIsAppImportanceLocked(PKG, UID));
 
         NotificationChannel defaultChannel = mHelper.getNotificationChannel(
                 PKG, UID, NotificationChannel.DEFAULT_CHANNEL_ID, false);
@@ -814,6 +817,7 @@
         defaultChannel.setBypassDnd(true);
         defaultChannel.setLockscreenVisibility(Notification.VISIBILITY_SECRET);
 
+        mHelper.setAppImportanceLocked(PKG, UID);
         mHelper.updateNotificationChannel(PKG, UID, defaultChannel, true);
 
         // ensure app level fields are changed
@@ -821,6 +825,7 @@
         assertEquals(Notification.PRIORITY_MAX, mHelper.getPackagePriority(PKG, UID));
         assertEquals(Notification.VISIBILITY_SECRET, mHelper.getPackageVisibility(PKG, UID));
         assertEquals(IMPORTANCE_NONE, mHelper.getImportance(PKG, UID));
+        assertTrue(mHelper.getIsAppImportanceLocked(PKG, UID));
     }
 
     @Test